一、如何签名以及验证签名?
签名
- 首先将要签名的message拼接起来
- 对message进行hash运算
- 将hash结果进行签名(在链下签名,需要用到私钥,一般都是需要调用钱包插件来进行签名);
验证签名
- 对message重新进行hash运算;
- 通过签名和hash,来反算出signer;这一步主要用到了
ecrecover
方法 - 比较signer是不是一致的
二、签名有哪些类型
Metamask提供了几种不同的签名方法,整个历史变化如下:
1、eth_sign==(危险)==
可以签名任意数据,但是很危险,已经不推荐使用。metamask也会有危险提示:
2、personal_sign
该方法在任何签名数据前都会加上\x19Ethereum Signed Message:\n
,
这意味着如果有人要签署交易数据,添加的前缀字符串会使其成为无效交易。
1.1、personal_sign 简单的对字符串进行签名
require('@nomiclabs/hardhat-waffle');const { expect } = require('chai');const { encrypt, recoverPersonalSignature, recoverTypedSignatureLegacy, recoverTypedSignature, recoverTypedSignature_v4 } = require('eth-sig-util');const { BigNumber, utils, provider } = ethers;const { solidityPack, concat, toUtf8Bytes, keccak256, SigningKey, formatBytes32String } = utils;describe('VerifySignature', () => {let contract;it('personal_sign', async () => {const [signer] = await ethers.getSigners();console.log('signer address = ', signer.address);const message = 'Example `personal_sign` message';// 直接用signMessage方法const signature = await signer.signMessage(signMessage);const hashedMessage = `0x${Buffer.from(signMessage, 'utf8').toString('hex')}`;const recoveredAddr = recoverPersonalSignature({data: hashedMessage,signature: signature,});// 调用ethers.utils的方法,对字符串hashconst hashedMessage = utils.keccak256(utils.toUtf8Bytes(message));console.log(`hashedMessage = `, hashedMessage);// 调用钱包的personal_sign方法,进行签名const signature = await provider.send('personal_sign', [hashedMessage, signer.address]);console.log(`signature = `, signature);// 调用eth-sig-util(recoverPersonalSignature)方法,验证签名const recoveredAddr = recoverPersonalSignature({data: hashedMessage,sig: signature,});console.log('recoveredAddr = ', recoveredAddr);expect(await signer.address.toUpperCase()).to.equal(recoveredAddr.toUpperCase());// 调用合约的verify方法,验证签名const recoverResult = await contract.verify(message, signature);console.log('recoverResult = ', recoverResult);expect(await signer.address.toUpperCase()).to.equal(recoverResult.toUpperCase());});});require('@nomiclabs/hardhat-waffle'); const { expect } = require('chai'); const { encrypt, recoverPersonalSignature, recoverTypedSignatureLegacy, recoverTypedSignature, recoverTypedSignature_v4 } = require('eth-sig-util'); const { BigNumber, utils, provider } = ethers; const { solidityPack, concat, toUtf8Bytes, keccak256, SigningKey, formatBytes32String } = utils; describe('VerifySignature', () => { let contract; it('personal_sign', async () => { const [signer] = await ethers.getSigners(); console.log('signer address = ', signer.address); const message = 'Example `personal_sign` message'; // 直接用signMessage方法 const signature = await signer.signMessage(signMessage); const hashedMessage = `0x${Buffer.from(signMessage, 'utf8').toString('hex')}`; const recoveredAddr = recoverPersonalSignature({ data: hashedMessage, signature: signature, }); // 调用ethers.utils的方法,对字符串hash const hashedMessage = utils.keccak256(utils.toUtf8Bytes(message)); console.log(`hashedMessage = `, hashedMessage); // 调用钱包的personal_sign方法,进行签名 const signature = await provider.send('personal_sign', [hashedMessage, signer.address]); console.log(`signature = `, signature); // 调用eth-sig-util(recoverPersonalSignature)方法,验证签名 const recoveredAddr = recoverPersonalSignature({ data: hashedMessage, sig: signature, }); console.log('recoveredAddr = ', recoveredAddr); expect(await signer.address.toUpperCase()).to.equal(recoveredAddr.toUpperCase()); // 调用合约的verify方法,验证签名 const recoverResult = await contract.verify(message, signature); console.log('recoverResult = ', recoverResult); expect(await signer.address.toUpperCase()).to.equal(recoverResult.toUpperCase()); }); });require('@nomiclabs/hardhat-waffle'); const { expect } = require('chai'); const { encrypt, recoverPersonalSignature, recoverTypedSignatureLegacy, recoverTypedSignature, recoverTypedSignature_v4 } = require('eth-sig-util'); const { BigNumber, utils, provider } = ethers; const { solidityPack, concat, toUtf8Bytes, keccak256, SigningKey, formatBytes32String } = utils; describe('VerifySignature', () => { let contract; it('personal_sign', async () => { const [signer] = await ethers.getSigners(); console.log('signer address = ', signer.address); const message = 'Example `personal_sign` message'; // 直接用signMessage方法 const signature = await signer.signMessage(signMessage); const hashedMessage = `0x${Buffer.from(signMessage, 'utf8').toString('hex')}`; const recoveredAddr = recoverPersonalSignature({ data: hashedMessage, signature: signature, }); // 调用ethers.utils的方法,对字符串hash const hashedMessage = utils.keccak256(utils.toUtf8Bytes(message)); console.log(`hashedMessage = `, hashedMessage); // 调用钱包的personal_sign方法,进行签名 const signature = await provider.send('personal_sign', [hashedMessage, signer.address]); console.log(`signature = `, signature); // 调用eth-sig-util(recoverPersonalSignature)方法,验证签名 const recoveredAddr = recoverPersonalSignature({ data: hashedMessage, sig: signature, }); console.log('recoveredAddr = ', recoveredAddr); expect(await signer.address.toUpperCase()).to.equal(recoveredAddr.toUpperCase()); // 调用合约的verify方法,验证签名 const recoverResult = await contract.verify(message, signature); console.log('recoverResult = ', recoverResult); expect(await signer.address.toUpperCase()).to.equal(recoverResult.toUpperCase()); }); });
1.2 合约里验证签名的方法
合约里主要是通过ecrecover
方法,来得到签名的address。
但是出于安全原因,推荐使用ECDSA.recover
方法,封装过很多逻辑;
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract VerifySignature {function getMessageHash(string memory _message) public pure returns (bytes32) {return keccak256(abi.encodePacked(_message));}function getEthSignedMessageHash(bytes32 _messageHash) public pure returns (bytes32) {/*Signature is produced by signing a keccak256 hash with the following format:"\x19Ethereum Signed Message\n" + len(msg) + msg*/return keccak256(abi.encodePacked('\x19Ethereum Signed Message:\n32', _messageHash));}function verify(string memory _message, bytes memory signature) public pure returns (address) {// 1、先生成hashbytes32 messageHash = getMessageHash(_message);// 2、生成一个eth signed 的hashbytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);// 3、从hash里计算出signer// return recoverSigner(ethSignedMessageHash, signature);// 更安全return ECDSA.recover(ethSignedMessageHash, signature);}// verify2直接借用了ECDSA的方法,来返回signerfunction verify2(bytes32 _hash, bytes memory _signature) public pure returns (address) {return _hash.toEthSignedMessageHash().recover(_signature);}function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature) public pure returns (address) {(bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature);return ecrecover(_ethSignedMessageHash, v, r, s);}// 计算rsvfunction splitSignature(bytes memory sig)publicpurereturns (bytes32 r,bytes32 s,uint8 v){require(sig.length == 65, 'invalid signature length');assembly {/*First 32 bytes stores the length of the signatureadd(sig, 32) = pointer of sig + 32effectively, skips first 32 bytes of signaturemload(p) loads next 32 bytes starting at the memory address p into memory*/// first 32 bytes, after the length prefixr := mload(add(sig, 32))// second 32 bytess := mload(add(sig, 64))// final byte (first byte of the next 32 bytes)v := byte(0, mload(add(sig, 96)))}// implicitly return (r, s, v)}}// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract VerifySignature { function getMessageHash(string memory _message) public pure returns (bytes32) { return keccak256(abi.encodePacked(_message)); } function getEthSignedMessageHash(bytes32 _messageHash) public pure returns (bytes32) { /* Signature is produced by signing a keccak256 hash with the following format: "\x19Ethereum Signed Message\n" + len(msg) + msg */ return keccak256(abi.encodePacked('\x19Ethereum Signed Message:\n32', _messageHash)); } function verify(string memory _message, bytes memory signature) public pure returns (address) { // 1、先生成hash bytes32 messageHash = getMessageHash(_message); // 2、生成一个eth signed 的hash bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash); // 3、从hash里计算出signer // return recoverSigner(ethSignedMessageHash, signature); // 更安全 return ECDSA.recover(ethSignedMessageHash, signature); } // verify2直接借用了ECDSA的方法,来返回signer function verify2(bytes32 _hash, bytes memory _signature) public pure returns (address) { return _hash.toEthSignedMessageHash().recover(_signature); } function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature) public pure returns (address) { (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); return ecrecover(_ethSignedMessageHash, v, r, s); } // 计算rsv function splitSignature(bytes memory sig) public pure returns ( bytes32 r, bytes32 s, uint8 v ) { require(sig.length == 65, 'invalid signature length'); assembly { /* First 32 bytes stores the length of the signature add(sig, 32) = pointer of sig + 32 effectively, skips first 32 bytes of signature mload(p) loads next 32 bytes starting at the memory address p into memory */ // first 32 bytes, after the length prefix r := mload(add(sig, 32)) // second 32 bytes s := mload(add(sig, 64)) // final byte (first byte of the next 32 bytes) v := byte(0, mload(add(sig, 96))) } // implicitly return (r, s, v) } }// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract VerifySignature { function getMessageHash(string memory _message) public pure returns (bytes32) { return keccak256(abi.encodePacked(_message)); } function getEthSignedMessageHash(bytes32 _messageHash) public pure returns (bytes32) { /* Signature is produced by signing a keccak256 hash with the following format: "\x19Ethereum Signed Message\n" + len(msg) + msg */ return keccak256(abi.encodePacked('\x19Ethereum Signed Message:\n32', _messageHash)); } function verify(string memory _message, bytes memory signature) public pure returns (address) { // 1、先生成hash bytes32 messageHash = getMessageHash(_message); // 2、生成一个eth signed 的hash bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash); // 3、从hash里计算出signer // return recoverSigner(ethSignedMessageHash, signature); // 更安全 return ECDSA.recover(ethSignedMessageHash, signature); } // verify2直接借用了ECDSA的方法,来返回signer function verify2(bytes32 _hash, bytes memory _signature) public pure returns (address) { return _hash.toEthSignedMessageHash().recover(_signature); } function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature) public pure returns (address) { (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); return ecrecover(_ethSignedMessageHash, v, r, s); } // 计算rsv function splitSignature(bytes memory sig) public pure returns ( bytes32 r, bytes32 s, uint8 v ) { require(sig.length == 65, 'invalid signature length'); assembly { /* First 32 bytes stores the length of the signature add(sig, 32) = pointer of sig + 32 effectively, skips first 32 bytes of signature mload(p) loads next 32 bytes starting at the memory address p into memory */ // first 32 bytes, after the length prefix r := mload(add(sig, 32)) // second 32 bytes s := mload(add(sig, 64)) // final byte (first byte of the next 32 bytes) v := byte(0, mload(add(sig, 96))) } // implicitly return (r, s, v) } }
1.3 对多个不同类型的参数加签
const { BigNumber, utils, provider } = ethers;//solidityKeccak256const types = ['bytes', 'bytes', 'address', 'string', 'string', 'uint256'];const values = ['0x19', '0x00', '0x8ef9f0acfef3d9ab023812bb889a8f5a214b9b82', '测试', '{}', 1];const hashedMessage = utils.solidityKeccak256(types, values);// 向当前钱包发起rpc请求;const signature = await provider.send('personal_sign', [hashedMessage, signer.address]);const { BigNumber, utils, provider } = ethers; //solidityKeccak256 const types = ['bytes', 'bytes', 'address', 'string', 'string', 'uint256']; const values = ['0x19', '0x00', '0x8ef9f0acfef3d9ab023812bb889a8f5a214b9b82', '测试', '{}', 1]; const hashedMessage = utils.solidityKeccak256(types, values); // 向当前钱包发起rpc请求; const signature = await provider.send('personal_sign', [hashedMessage, signer.address]);const { BigNumber, utils, provider } = ethers; //solidityKeccak256 const types = ['bytes', 'bytes', 'address', 'string', 'string', 'uint256']; const values = ['0x19', '0x00', '0x8ef9f0acfef3d9ab023812bb889a8f5a214b9b82', '测试', '{}', 1]; const hashedMessage = utils.solidityKeccak256(types, values); // 向当前钱包发起rpc请求; const signature = await provider.send('personal_sign', [hashedMessage, signer.address]);
keccak256(solidityPack)
和 solidityKeccak256
这两种写法相同
const { BigNumber, utils, provider } = ethers;const types = ['address', 'address', 'uint256', 'address', 'uint256', 'uint256', 'uint256'];const values = [forkDelta.address, baseToken, baseAmount, quoteToken, quoteAmount, expires, orderNonce];const hashedMessage1 = utils.keccak256(utils.solidityPack(types, values));const hashedMessage2 = utils.solidityKeccak256(types, values);console.log(hashedMessage1 == hashedMessage2);const { BigNumber, utils, provider } = ethers; const types = ['address', 'address', 'uint256', 'address', 'uint256', 'uint256', 'uint256']; const values = [forkDelta.address, baseToken, baseAmount, quoteToken, quoteAmount, expires, orderNonce]; const hashedMessage1 = utils.keccak256(utils.solidityPack(types, values)); const hashedMessage2 = utils.solidityKeccak256(types, values); console.log(hashedMessage1 == hashedMessage2);const { BigNumber, utils, provider } = ethers; const types = ['address', 'address', 'uint256', 'address', 'uint256', 'uint256', 'uint256']; const values = [forkDelta.address, baseToken, baseAmount, quoteToken, quoteAmount, expires, orderNonce]; const hashedMessage1 = utils.keccak256(utils.solidityPack(types, values)); const hashedMessage2 = utils.solidityKeccak256(types, values); console.log(hashedMessage1 == hashedMessage2);
在web3js里可以这么写
const hashedMessage = web3.utils.soliditySha3(forkDelta.address,baseToken,baseAmount.toString(),quoteToken,quoteAmount.toString(),expires,orderNonce);const hashedMessage = web3.utils.soliditySha3( forkDelta.address, baseToken, baseAmount.toString(), quoteToken, quoteAmount.toString(), expires, orderNonce );const hashedMessage = web3.utils.soliditySha3( forkDelta.address, baseToken, baseAmount.toString(), quoteToken, quoteAmount.toString(), expires, orderNonce );
在合约里可以这样实现
function prefixed(bytes32 hash) internal pure returns (bytes32) {return keccak256(abi.encodePacked('\x19Ethereum Signed Message:\n32', hash));}function verify(OrderSigned memory orderSigned, uint256 amount) public {bytes32 hash = keccak256(abi.encodePacked(address(this),orderSigned.baseToken,orderSigned.baseAmount,orderSigned.quoteToken,orderSigned.quoteAmount,orderSigned.expires,orderSigned.nonce));console.logBytes32(hash);bytes32 messageDigest = prefixed(hash);// address signer = ecrecover(messageDigest, orderSigned.v, orderSigned.r, orderSigned.s);address signer = ECDSA.recover(messageDigest, orderSigned.signature);}function prefixed(bytes32 hash) internal pure returns (bytes32) { return keccak256(abi.encodePacked('\x19Ethereum Signed Message:\n32', hash)); } function verify(OrderSigned memory orderSigned, uint256 amount) public { bytes32 hash = keccak256( abi.encodePacked( address(this), orderSigned.baseToken, orderSigned.baseAmount, orderSigned.quoteToken, orderSigned.quoteAmount, orderSigned.expires, orderSigned.nonce ) ); console.logBytes32(hash); bytes32 messageDigest = prefixed(hash); // address signer = ecrecover(messageDigest, orderSigned.v, orderSigned.r, orderSigned.s); address signer = ECDSA.recover(messageDigest, orderSigned.signature); }function prefixed(bytes32 hash) internal pure returns (bytes32) { return keccak256(abi.encodePacked('\x19Ethereum Signed Message:\n32', hash)); } function verify(OrderSigned memory orderSigned, uint256 amount) public { bytes32 hash = keccak256( abi.encodePacked( address(this), orderSigned.baseToken, orderSigned.baseAmount, orderSigned.quoteToken, orderSigned.quoteAmount, orderSigned.expires, orderSigned.nonce ) ); console.logBytes32(hash); bytes32 messageDigest = prefixed(hash); // address signer = ecrecover(messageDigest, orderSigned.v, orderSigned.r, orderSigned.s); address signer = ECDSA.recover(messageDigest, orderSigned.signature); }
3、signTypedData
signTypedData属于符合EIP-712标准 [[【3-合约开发】0~合约里的一些ERC协议#EIP-712]]。他专门针对于一些交易的数据签名。并且有几个版本:
- signTypedData_v1
- signTypedData_v3
- signTypedData_v4
3.1、signTypedData_v1==(注:已经过时了)==
v1基于早期的EIP-712的协议,对数据的格式要求如下:
{type: 'array',items: {type: 'object',properties: {name: {type: 'string'},type: {type: 'string'}, // Solidity type as described here: https://github.com/ethereum/solidity/blob/93b1cc97022aa01e7daa9816bcc23108bbe008b5/libsolidity/ast/Types.cpp#L182value: {oneOf: [{type: 'string'},{type: 'number'},{type: 'boolean'},],},},},}{ type: 'array', items: { type: 'object', properties: { name: {type: 'string'}, type: {type: 'string'}, // Solidity type as described here: https://github.com/ethereum/solidity/blob/93b1cc97022aa01e7daa9816bcc23108bbe008b5/libsolidity/ast/Types.cpp#L182 value: { oneOf: [ {type: 'string'}, {type: 'number'}, {type: 'boolean'}, ], }, }, }, }{ type: 'array', items: { type: 'object', properties: { name: {type: 'string'}, type: {type: 'string'}, // Solidity type as described here: https://github.com/ethereum/solidity/blob/93b1cc97022aa01e7daa9816bcc23108bbe008b5/libsolidity/ast/Types.cpp#L182 value: { oneOf: [ {type: 'string'}, {type: 'number'}, {type: 'boolean'}, ], }, }, }, }
早期Web3.js里调用方式如下,现在找不到这个api去支持签名v1版本的数据了。
const typedData = [{'type': 'string','name': 'message','value': 'Hi, Alice!',},{'type': 'uint','name': 'value','value': 42,},];// 找不到了const signature = await web3.personal.signTypedData(typedData);const typedData = [ { 'type': 'string', 'name': 'message', 'value': 'Hi, Alice!', }, { 'type': 'uint', 'name': 'value', 'value': 42, }, ]; // 找不到了 const signature = await web3.personal.signTypedData(typedData);const typedData = [ { 'type': 'string', 'name': 'message', 'value': 'Hi, Alice!', }, { 'type': 'uint', 'name': 'value', 'value': 42, }, ]; // 找不到了 const signature = await web3.personal.signTypedData(typedData);
如果想尝试,可以用matamask提供的eth_signTypedData
方法来唤起。效果如下:
通过eth-sig-util
来验证:
const {recoverTypedSignatureLegacy,signTypedDataLegacy,} = require('eth-sig-util');it('struct_sign_typed_data_v1', async () => {const [signer] = await ethers.getSigners();signer.privateKey = '10645fd201fc751be73c2d5219a2cd738418f2b15b90d1ea5fa2f422951af7a3';console.log('signer address = ', signer.address);const msgParams = [{type: 'string',name: 'Message',value: 'Hi, Alice!',},{type: 'uint32',name: 'Value',value: 1337,},];const privateKey1Buffer = Buffer.from(signer.privateKey, 'hex');const signature = signTypedDataLegacy(privateKey1Buffer, { data: msgParams });// 如果是metmask插件,可以调用下面的方法// const signature = await provider.send('eth_signTypedData', [msgParams, signer.address]);const jsRecoveredAddr = recoverTypedSignatureLegacy({data: msgParams,sig: signature,});console.log('jsRecoveredAddr = ', jsRecoveredAddr);expect(signer.address.toUpperCase()).to.equal(jsRecoveredAddr.toUpperCase());});const { recoverTypedSignatureLegacy, signTypedDataLegacy, } = require('eth-sig-util'); it('struct_sign_typed_data_v1', async () => { const [signer] = await ethers.getSigners(); signer.privateKey = '10645fd201fc751be73c2d5219a2cd738418f2b15b90d1ea5fa2f422951af7a3'; console.log('signer address = ', signer.address); const msgParams = [ { type: 'string', name: 'Message', value: 'Hi, Alice!', }, { type: 'uint32', name: 'Value', value: 1337, }, ]; const privateKey1Buffer = Buffer.from(signer.privateKey, 'hex'); const signature = signTypedDataLegacy(privateKey1Buffer, { data: msgParams }); // 如果是metmask插件,可以调用下面的方法 // const signature = await provider.send('eth_signTypedData', [msgParams, signer.address]); const jsRecoveredAddr = recoverTypedSignatureLegacy({ data: msgParams, sig: signature, }); console.log('jsRecoveredAddr = ', jsRecoveredAddr); expect(signer.address.toUpperCase()).to.equal(jsRecoveredAddr.toUpperCase()); });const { recoverTypedSignatureLegacy, signTypedDataLegacy, } = require('eth-sig-util'); it('struct_sign_typed_data_v1', async () => { const [signer] = await ethers.getSigners(); signer.privateKey = '10645fd201fc751be73c2d5219a2cd738418f2b15b90d1ea5fa2f422951af7a3'; console.log('signer address = ', signer.address); const msgParams = [ { type: 'string', name: 'Message', value: 'Hi, Alice!', }, { type: 'uint32', name: 'Value', value: 1337, }, ]; const privateKey1Buffer = Buffer.from(signer.privateKey, 'hex'); const signature = signTypedDataLegacy(privateKey1Buffer, { data: msgParams }); // 如果是metmask插件,可以调用下面的方法 // const signature = await provider.send('eth_signTypedData', [msgParams, signer.address]); const jsRecoveredAddr = recoverTypedSignatureLegacy({ data: msgParams, sig: signature, }); console.log('jsRecoveredAddr = ', jsRecoveredAddr); expect(signer.address.toUpperCase()).to.equal(jsRecoveredAddr.toUpperCase()); });
3.2、signTypedData_v3
v3版本是基于EIP-712协议,除了对数组和嵌套数据结构不支持
(except that arrays and recursive data structures are not supported.)
const msgParams = {types: {Person: [{ name: 'name', type: 'string' },{ name: 'wallet', type: 'address' },],Mail: [{ name: 'from', type: 'Person' },{ name: 'to', type: 'Person' },{ name: 'contents', type: 'string' },],},primaryType: 'Mail',domain: {name: 'Ether Mail',version: '1',chainId,verifyingContract: contract.address,},message: {from: {name: 'Cow',wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',},to: {name: 'Bob',wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',},contents: 'Hello, Bob!',},};it('struct_sign_typed_data_v3', async () => {const [signer] = await ethers.getSigners();// console.log('signer address = ', signer.address);const network = await provider.getNetwork();const chainId = network.chainId;// console.log('chainId = ', network.chainId);// console.log(`contract address = `, contract.address);const signature = await signer._signTypedData(msgParams.domain, msgParams.types, msgParams.message);console.log(`signature = `, signature);// 如果是metmask插件,可以调用下面的方法// const signature = await provider.send('eth_signTypedData_v3', [signer.address, msgParams]);// js验证const jsRecoveredAddr = utils.verifyTypedData(msgParams.domain, msgParams.types, msgParams.message, signature);console.log('jsRecoveredAddr = ', jsRecoveredAddr);expect(signer.address.toUpperCase()).to.equal(jsRecoveredAddr.toUpperCase());// 合约验证const contractRecoveredResult = await contract.verify3(msgParams.message, signature);console.log('contractRecoveredResult = ', contractRecoveredResult);expect(await signer.address.toUpperCase()).to.equal(contractRecoveredResult.toUpperCase());});const msgParams = { types: { Person: [ { name: 'name', type: 'string' }, { name: 'wallet', type: 'address' }, ], Mail: [ { name: 'from', type: 'Person' }, { name: 'to', type: 'Person' }, { name: 'contents', type: 'string' }, ], }, primaryType: 'Mail', domain: { name: 'Ether Mail', version: '1', chainId, verifyingContract: contract.address, }, message: { from: { name: 'Cow', wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', }, to: { name: 'Bob', wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', }, contents: 'Hello, Bob!', }, }; it('struct_sign_typed_data_v3', async () => { const [signer] = await ethers.getSigners(); // console.log('signer address = ', signer.address); const network = await provider.getNetwork(); const chainId = network.chainId; // console.log('chainId = ', network.chainId); // console.log(`contract address = `, contract.address); const signature = await signer._signTypedData(msgParams.domain, msgParams.types, msgParams.message); console.log(`signature = `, signature); // 如果是metmask插件,可以调用下面的方法 // const signature = await provider.send('eth_signTypedData_v3', [signer.address, msgParams]); // js验证 const jsRecoveredAddr = utils.verifyTypedData(msgParams.domain, msgParams.types, msgParams.message, signature); console.log('jsRecoveredAddr = ', jsRecoveredAddr); expect(signer.address.toUpperCase()).to.equal(jsRecoveredAddr.toUpperCase()); // 合约验证 const contractRecoveredResult = await contract.verify3(msgParams.message, signature); console.log('contractRecoveredResult = ', contractRecoveredResult); expect(await signer.address.toUpperCase()).to.equal(contractRecoveredResult.toUpperCase()); });const msgParams = { types: { Person: [ { name: 'name', type: 'string' }, { name: 'wallet', type: 'address' }, ], Mail: [ { name: 'from', type: 'Person' }, { name: 'to', type: 'Person' }, { name: 'contents', type: 'string' }, ], }, primaryType: 'Mail', domain: { name: 'Ether Mail', version: '1', chainId, verifyingContract: contract.address, }, message: { from: { name: 'Cow', wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', }, to: { name: 'Bob', wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', }, contents: 'Hello, Bob!', }, }; it('struct_sign_typed_data_v3', async () => { const [signer] = await ethers.getSigners(); // console.log('signer address = ', signer.address); const network = await provider.getNetwork(); const chainId = network.chainId; // console.log('chainId = ', network.chainId); // console.log(`contract address = `, contract.address); const signature = await signer._signTypedData(msgParams.domain, msgParams.types, msgParams.message); console.log(`signature = `, signature); // 如果是metmask插件,可以调用下面的方法 // const signature = await provider.send('eth_signTypedData_v3', [signer.address, msgParams]); // js验证 const jsRecoveredAddr = utils.verifyTypedData(msgParams.domain, msgParams.types, msgParams.message, signature); console.log('jsRecoveredAddr = ', jsRecoveredAddr); expect(signer.address.toUpperCase()).to.equal(jsRecoveredAddr.toUpperCase()); // 合约验证 const contractRecoveredResult = await contract.verify3(msgParams.message, signature); console.log('contractRecoveredResult = ', contractRecoveredResult); expect(await signer.address.toUpperCase()).to.equal(contractRecoveredResult.toUpperCase()); });
合约代码:
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;pragma experimental ABIEncoderV2;import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol';import '@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol';import 'hardhat/console.sol';contract VerifySignature is EIP712 {using ECDSA for bytes32;// 这里的domain version要和前端传的一致constructor() EIP712('Ether Mail', '1') {}/**Person: [{ name: 'name', type: 'string' },{ name: 'wallet', type: 'address' },],Mail: [{ name: 'from', type: 'Person' },{ name: 'to', type: 'Person' },{ name: 'contents', type: 'string' },],*/struct Person {string name;address wallet;}struct Mail {Person from;Person to;string contents;}bytes32 constant MAIL_TYPE_HASH = keccak256('Mail(Person from,Person to,string contents)Person(string name,address wallet)');bytes32 PERSON_TYPE_HASH = keccak256('Person(string name,address wallet)');function hash(Person calldata person) internal view returns (bytes32) {return keccak256(abi.encode(PERSON_TYPE_HASH, keccak256(bytes(person.name)), person.wallet));}function hash(Mail calldata mail) internal view returns (bytes32) {return keccak256(abi.encode(MAIL_TYPE_HASH, hash(mail.from), hash(mail.to), keccak256(bytes(mail.contents))));}function verify3(Mail calldata mail, bytes memory _signature) public view returns (address) {bytes32 structHash = hash(mail);bytes32 digest = _hashTypedDataV4(structHash);address signer = ECDSA.recover(digest, _signature);return signer;}}// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; pragma experimental ABIEncoderV2; import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; import '@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol'; import 'hardhat/console.sol'; contract VerifySignature is EIP712 { using ECDSA for bytes32; // 这里的domain version要和前端传的一致 constructor() EIP712('Ether Mail', '1') {} /** Person: [ { name: 'name', type: 'string' }, { name: 'wallet', type: 'address' }, ], Mail: [ { name: 'from', type: 'Person' }, { name: 'to', type: 'Person' }, { name: 'contents', type: 'string' }, ], */ struct Person { string name; address wallet; } struct Mail { Person from; Person to; string contents; } bytes32 constant MAIL_TYPE_HASH = keccak256('Mail(Person from,Person to,string contents)Person(string name,address wallet)'); bytes32 PERSON_TYPE_HASH = keccak256('Person(string name,address wallet)'); function hash(Person calldata person) internal view returns (bytes32) { return keccak256(abi.encode(PERSON_TYPE_HASH, keccak256(bytes(person.name)), person.wallet)); } function hash(Mail calldata mail) internal view returns (bytes32) { return keccak256(abi.encode(MAIL_TYPE_HASH, hash(mail.from), hash(mail.to), keccak256(bytes(mail.contents)))); } function verify3(Mail calldata mail, bytes memory _signature) public view returns (address) { bytes32 structHash = hash(mail); bytes32 digest = _hashTypedDataV4(structHash); address signer = ECDSA.recover(digest, _signature); return signer; } }// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; pragma experimental ABIEncoderV2; import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; import '@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol'; import 'hardhat/console.sol'; contract VerifySignature is EIP712 { using ECDSA for bytes32; // 这里的domain version要和前端传的一致 constructor() EIP712('Ether Mail', '1') {} /** Person: [ { name: 'name', type: 'string' }, { name: 'wallet', type: 'address' }, ], Mail: [ { name: 'from', type: 'Person' }, { name: 'to', type: 'Person' }, { name: 'contents', type: 'string' }, ], */ struct Person { string name; address wallet; } struct Mail { Person from; Person to; string contents; } bytes32 constant MAIL_TYPE_HASH = keccak256('Mail(Person from,Person to,string contents)Person(string name,address wallet)'); bytes32 PERSON_TYPE_HASH = keccak256('Person(string name,address wallet)'); function hash(Person calldata person) internal view returns (bytes32) { return keccak256(abi.encode(PERSON_TYPE_HASH, keccak256(bytes(person.name)), person.wallet)); } function hash(Mail calldata mail) internal view returns (bytes32) { return keccak256(abi.encode(MAIL_TYPE_HASH, hash(mail.from), hash(mail.to), keccak256(bytes(mail.contents)))); } function verify3(Mail calldata mail, bytes memory _signature) public view returns (address) { bytes32 structHash = hash(mail); bytes32 digest = _hashTypedDataV4(structHash); address signer = ECDSA.recover(digest, _signature); return signer; } }
3.3、signTypedData_v4
v4版本是基于EIP-712协议,是最新的,支持所有的数据格式,包括数组和嵌套的struct。
const msgParams = {domain: {version: '1',name: 'Ether Mail',chainId,verifyingContract: contract.address,},message: {data: ['1', '2', '3'],},primaryType: 'Message',types: {Message: [{ name: 'data', type: 'string[]' }],},};it('struct_sign_typed_data_v4', async () => {const [signer] = await ethers.getSigners();// console.log('signer address = ', signer.address);const network = await provider.getNetwork();const chainId = network.chainId;// console.log('chainId = ', network.chainId);// console.log(`contract address = `, contract.address);const signature = await signer._signTypedData(msgParams.domain, msgParams.types, msgParams.message);console.log(`signature = `, signature);// 如果是metmask插件,可以调用下面的方法// const signature = await provider.send('eth_signTypedData_v4', [signer.address, msgParams]);// js验证const jsRecoveredAddr = utils.verifyTypedData(msgParams.domain, msgParams.types, msgParams.message, signature);console.log('jsRecoveredAddr = ', jsRecoveredAddr);expect(signer.address.toUpperCase()).to.equal(jsRecoveredAddr.toUpperCase());// 合约验证const contractRecoveredResult = await contract.verify4(msgParams.message, signature);console.log('contractRecoveredResult = ', contractRecoveredResult);expect(await signer.address.toUpperCase()).to.equal(contractRecoveredResult.toUpperCase());});const msgParams = { domain: { version: '1', name: 'Ether Mail', chainId, verifyingContract: contract.address, }, message: { data: ['1', '2', '3'], }, primaryType: 'Message', types: { Message: [{ name: 'data', type: 'string[]' }], }, }; it('struct_sign_typed_data_v4', async () => { const [signer] = await ethers.getSigners(); // console.log('signer address = ', signer.address); const network = await provider.getNetwork(); const chainId = network.chainId; // console.log('chainId = ', network.chainId); // console.log(`contract address = `, contract.address); const signature = await signer._signTypedData(msgParams.domain, msgParams.types, msgParams.message); console.log(`signature = `, signature); // 如果是metmask插件,可以调用下面的方法 // const signature = await provider.send('eth_signTypedData_v4', [signer.address, msgParams]); // js验证 const jsRecoveredAddr = utils.verifyTypedData(msgParams.domain, msgParams.types, msgParams.message, signature); console.log('jsRecoveredAddr = ', jsRecoveredAddr); expect(signer.address.toUpperCase()).to.equal(jsRecoveredAddr.toUpperCase()); // 合约验证 const contractRecoveredResult = await contract.verify4(msgParams.message, signature); console.log('contractRecoveredResult = ', contractRecoveredResult); expect(await signer.address.toUpperCase()).to.equal(contractRecoveredResult.toUpperCase()); });const msgParams = { domain: { version: '1', name: 'Ether Mail', chainId, verifyingContract: contract.address, }, message: { data: ['1', '2', '3'], }, primaryType: 'Message', types: { Message: [{ name: 'data', type: 'string[]' }], }, }; it('struct_sign_typed_data_v4', async () => { const [signer] = await ethers.getSigners(); // console.log('signer address = ', signer.address); const network = await provider.getNetwork(); const chainId = network.chainId; // console.log('chainId = ', network.chainId); // console.log(`contract address = `, contract.address); const signature = await signer._signTypedData(msgParams.domain, msgParams.types, msgParams.message); console.log(`signature = `, signature); // 如果是metmask插件,可以调用下面的方法 // const signature = await provider.send('eth_signTypedData_v4', [signer.address, msgParams]); // js验证 const jsRecoveredAddr = utils.verifyTypedData(msgParams.domain, msgParams.types, msgParams.message, signature); console.log('jsRecoveredAddr = ', jsRecoveredAddr); expect(signer.address.toUpperCase()).to.equal(jsRecoveredAddr.toUpperCase()); // 合约验证 const contractRecoveredResult = await contract.verify4(msgParams.message, signature); console.log('contractRecoveredResult = ', contractRecoveredResult); expect(await signer.address.toUpperCase()).to.equal(contractRecoveredResult.toUpperCase()); });
合约代码:
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;pragma experimental ABIEncoderV2;import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol';import '@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol';import 'hardhat/console.sol';contract VerifySignature is EIP712 {using ECDSA for bytes32;// domain versionconstructor() EIP712('Ether Mail', '1') {}struct Message {string[] data;}bytes32 constant Message_TYPE_HASH = keccak256('Message(string[] data)');function hash(Message calldata message) internal view returns (bytes32) {bytes32[] memory keccakData = new bytes32[](message.data.length);for (uint256 i = 0; i < message.data.length; i++) {keccakData[i] = keccak256(bytes(message.data[i]));}bytes32 computedHash5 = keccak256(abi.encodePacked(keccakData));return keccak256(abi.encode(Message_TYPE_HASH, computedHash5));}function verify4(Message calldata message, bytes memory _signature) public view returns (address) {console.log(message.data.length);bytes32 structHash = hash(message);bytes32 digest = _hashTypedDataV4(structHash);address signer = ECDSA.recover(digest, _signature);return signer;}}// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; pragma experimental ABIEncoderV2; import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; import '@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol'; import 'hardhat/console.sol'; contract VerifySignature is EIP712 { using ECDSA for bytes32; // domain version constructor() EIP712('Ether Mail', '1') {} struct Message { string[] data; } bytes32 constant Message_TYPE_HASH = keccak256('Message(string[] data)'); function hash(Message calldata message) internal view returns (bytes32) { bytes32[] memory keccakData = new bytes32[](message.data.length); for (uint256 i = 0; i < message.data.length; i++) { keccakData[i] = keccak256(bytes(message.data[i])); } bytes32 computedHash5 = keccak256(abi.encodePacked(keccakData)); return keccak256(abi.encode(Message_TYPE_HASH, computedHash5)); } function verify4(Message calldata message, bytes memory _signature) public view returns (address) { console.log(message.data.length); bytes32 structHash = hash(message); bytes32 digest = _hashTypedDataV4(structHash); address signer = ECDSA.recover(digest, _signature); return signer; } }// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; pragma experimental ABIEncoderV2; import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; import '@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol'; import 'hardhat/console.sol'; contract VerifySignature is EIP712 { using ECDSA for bytes32; // domain version constructor() EIP712('Ether Mail', '1') {} struct Message { string[] data; } bytes32 constant Message_TYPE_HASH = keccak256('Message(string[] data)'); function hash(Message calldata message) internal view returns (bytes32) { bytes32[] memory keccakData = new bytes32[](message.data.length); for (uint256 i = 0; i < message.data.length; i++) { keccakData[i] = keccak256(bytes(message.data[i])); } bytes32 computedHash5 = keccak256(abi.encodePacked(keccakData)); return keccak256(abi.encode(Message_TYPE_HASH, computedHash5)); } function verify4(Message calldata message, bytes memory _signature) public view returns (address) { console.log(message.data.length); bytes32 structHash = hash(message); bytes32 digest = _hashTypedDataV4(structHash); address signer = ECDSA.recover(digest, _signature); return signer; } }
3.2 示例Demo
利用钱包或者插件,在链下签名。
然后在链上合约进行签名的验证;
三、签名 r/s/v (做个了解)
r,s,v是ecreover方法里的参数,这个是通过签名计算出来的。
也可以在合约里计算,参考上面的代码。
// split signature// 这里的r/s/v是签名算法的偏移量,会在验证签名时用到,做个了解const r = signature.slice(0, 66);const s = "0x" + signature.slice(66, 130);const v = parseInt(signature.slice(130, 132), 16);console.log({ r, s, v });// split signature // 这里的r/s/v是签名算法的偏移量,会在验证签名时用到,做个了解 const r = signature.slice(0, 66); const s = "0x" + signature.slice(66, 130); const v = parseInt(signature.slice(130, 132), 16); console.log({ r, s, v });// split signature // 这里的r/s/v是签名算法的偏移量,会在验证签名时用到,做个了解 const r = signature.slice(0, 66); const s = "0x" + signature.slice(66, 130); const v = parseInt(signature.slice(130, 132), 16); console.log({ r, s, v });
// Create a wallet to sign the hash withlet privateKey = '0x0123456789012345678901234567890123456789012345678901234567890123';let wallet = new ethers.Wallet(privateKey);console.log(wallet.address);// "0x14791697260E4c9A71f18484C9f997B308e59325"let contractAddress = '0x80F85dA065115F576F1fbe5E14285dA51ea39260';let contract = new ethers.Contract(contractAddress, abi, provider);// The hash we wish to sign and verifylet messageHash = ethers.utils.id("Hello World");// Note: messageHash is a string, that is 66-bytes long, to sign the// binary value, we must convert it to the 32 byte Array that// the string represents//// i.e.// // 66-byte string// "0x592fa743889fc7f92ac2a37bb1f5ba1daf2a5c84741ca0e0061d243a2e6707ba"//// ... vs ...//// // 32 entry Uint8Array// [ 89, 47, 167, 67, 136, 159, 199, 249, 42, 194, 163,// 123, 177, 245, 186, 29, 175, 42, 92, 132, 116, 28,// 160, 224, 6, 29, 36, 58, 46, 103, 7, 186]let messageHashBytes = ethers.utils.arrayify(messageHash)// Sign the binary datalet flatSig = await wallet.signMessage(messageHashBytes);// For Solidity, we need the expanded-format of a signature// sig.v, sig.r, sig.slet sig = ethers.utils.splitSignature(flatSig);// Create a wallet to sign the hash with let privateKey = '0x0123456789012345678901234567890123456789012345678901234567890123'; let wallet = new ethers.Wallet(privateKey); console.log(wallet.address); // "0x14791697260E4c9A71f18484C9f997B308e59325" let contractAddress = '0x80F85dA065115F576F1fbe5E14285dA51ea39260'; let contract = new ethers.Contract(contractAddress, abi, provider); // The hash we wish to sign and verify let messageHash = ethers.utils.id("Hello World"); // Note: messageHash is a string, that is 66-bytes long, to sign the // binary value, we must convert it to the 32 byte Array that // the string represents // // i.e. // // 66-byte string // "0x592fa743889fc7f92ac2a37bb1f5ba1daf2a5c84741ca0e0061d243a2e6707ba" // // ... vs ... // // // 32 entry Uint8Array // [ 89, 47, 167, 67, 136, 159, 199, 249, 42, 194, 163, // 123, 177, 245, 186, 29, 175, 42, 92, 132, 116, 28, // 160, 224, 6, 29, 36, 58, 46, 103, 7, 186] let messageHashBytes = ethers.utils.arrayify(messageHash) // Sign the binary data let flatSig = await wallet.signMessage(messageHashBytes); // For Solidity, we need the expanded-format of a signature // sig.v, sig.r, sig.s let sig = ethers.utils.splitSignature(flatSig);// Create a wallet to sign the hash with let privateKey = '0x0123456789012345678901234567890123456789012345678901234567890123'; let wallet = new ethers.Wallet(privateKey); console.log(wallet.address); // "0x14791697260E4c9A71f18484C9f997B308e59325" let contractAddress = '0x80F85dA065115F576F1fbe5E14285dA51ea39260'; let contract = new ethers.Contract(contractAddress, abi, provider); // The hash we wish to sign and verify let messageHash = ethers.utils.id("Hello World"); // Note: messageHash is a string, that is 66-bytes long, to sign the // binary value, we must convert it to the 32 byte Array that // the string represents // // i.e. // // 66-byte string // "0x592fa743889fc7f92ac2a37bb1f5ba1daf2a5c84741ca0e0061d243a2e6707ba" // // ... vs ... // // // 32 entry Uint8Array // [ 89, 47, 167, 67, 136, 159, 199, 249, 42, 194, 163, // 123, 177, 245, 186, 29, 175, 42, 92, 132, 116, 28, // 160, 224, 6, 29, 36, 58, 46, 103, 7, 186] let messageHashBytes = ethers.utils.arrayify(messageHash) // Sign the binary data let flatSig = await wallet.signMessage(messageHashBytes); // For Solidity, we need the expanded-format of a signature // sig.v, sig.r, sig.s let sig = ethers.utils.splitSignature(flatSig);
四、使用java的web3
假设有这么个结构,用web3j怎么实现?
const msgParams = {types: {swap: [{ name: 'sign_str', type: 'string' },]},primaryType: 'swap',domain: {name: 'Ether Mail',version: '1',chainId,verifyingContract: contract.address,},message: {sign_str: 'Hello, Bob!',},};const signature = await signer._signTypedData(msgParams.domain, msgParams.types, msgParams.message);console.log(`signature = `, signature);const msgParams = { types: { swap: [ { name: 'sign_str', type: 'string' }, ] }, primaryType: 'swap', domain: { name: 'Ether Mail', version: '1', chainId, verifyingContract: contract.address, }, message: { sign_str: 'Hello, Bob!', }, }; const signature = await signer._signTypedData(msgParams.domain, msgParams.types, msgParams.message); console.log(`signature = `, signature);const msgParams = { types: { swap: [ { name: 'sign_str', type: 'string' }, ] }, primaryType: 'swap', domain: { name: 'Ether Mail', version: '1', chainId, verifyingContract: contract.address, }, message: { sign_str: 'Hello, Bob!', }, }; const signature = await signer._signTypedData(msgParams.domain, msgParams.types, msgParams.message); console.log(`signature = `, signature);
最终实现
package com.xxxx.nft.bcs;import lombok.extern.slf4j.Slf4j;import org.web3j.crypto.*;import org.web3j.utils.Numeric;import java.math.BigInteger;/*** @author keyang* @Description TODO**/@Slf4jpublic class EIP712Utils {public static final byte[] EIP712DOMAIN_TYPEHASH = Hash.sha3("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)".getBytes());public static final byte[] MessageTypeHash = Hash.sha3("swap(string sign_str)".getBytes());// 生成domain hashpublic static String generateDomainSeparatorHash(String name, String version, Integer chainId, String verifyContractAddress) {// 省略代码}// 生成一个struct hashpublic static String generateStructHash(String signParams) {// 省略代码}public static String signEIP712Message(String privateKey, String domainSeparatorHashHex, String structHashHex) {String messageHash = signHash(domainSeparatorHashHex, structHashHex);log.info("messageHash={}", messageHash);byte[] messageHashBytes = Numeric.hexStringToByteArray(messageHash);Credentials credentials = Credentials.create(privateKey);Sign.SignatureData signatureData = Sign.signMessage(messageHashBytes, credentials.getEcKeyPair(), false);String r = Numeric.toHexString(signatureData.getR());String s = Numeric.toHexString(signatureData.getS());BigInteger v = Numeric.toBigInt(signatureData.getV());log.info("R={}", r);log.info("S={}", s);log.info("V={}", v);// 拼接签名String signature = Numeric.toHexString(signatureData.getR()) + Numeric.toHexStringNoPrefix(signatureData.getS()) + Numeric.toHexStringNoPrefix(signatureData.getV());return signature;}// doneprivate static String signHash(String domainSeparatorHashHex, String structHashHex) {byte[] buffer1 = Numeric.hexStringToByteArray(domainSeparatorHashHex); // Generate or provide buffer1byte[] buffer2 = Numeric.hexStringToByteArray(structHashHex); // Generate or provide buffer2byte[] concatenatedHash = Hash.sha3(concatenate(Numeric.hexStringToByteArray("1901"), buffer1, buffer2));return Numeric.toHexString(concatenatedHash);}// doneprivate static byte[] concatenate(byte[]... arrays) {int totalLength = 0;for (byte[] array : arrays) {totalLength += array.length;}byte[] result = new byte[totalLength];int currentIndex = 0;for (byte[] array : arrays) {System.arraycopy(array, 0, result, currentIndex, array.length);currentIndex += array.length;}return result;}public static void main(String[] args) throws Exception {// 私钥String privateKey1 = "0xc85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4";Credentials credentials = Credentials.create(privateKey1);// 生成地址String signerAddress = credentials.getAddress();log.info("Address: " + signerAddress);// 生成domainHashString domainSeparatorHash = generateDomainSeparatorHash("Nft Market", "1.0", 97, "0xCE83De204A9374CCA6b4c848565EBf0eEec85655");log.info("domainSeparatorHash:{}", domainSeparatorHash);String expectedDomainSeparatorHash = "0xd82ca19e5ab4788d66937bc55d3ba6262acfed0f62bb35e2cf5abde9d4c2929b";assert (expectedDomainSeparatorHash.equalsIgnoreCase(domainSeparatorHash));// struct hashString signParam = "0xca293EB20BFbb9A2cBde3EC4878cFaE06059f1aD|2|1000000000000000000|0x194852a4Fd96C57f7Ba1D35b3e711D0cFd93a977|1|0x2609841E44B5F418a9Ed2062dFe561BC430A5A43|1681280890|1691280890|asdf|1|97";String structHash = generateStructHash(signParam);log.info("structHash:{}", structHash);String exceptedStructHash = "0xc7e55c91bec99e28a920df095ac32e3fb2201523e18e9b862e9ff0d81307fbcc";assert (exceptedStructHash.equalsIgnoreCase(structHash));String signature = signEIP712Message(privateKey1,domainSeparatorHash,structHash);log.info("signature: " + signature);String expectedSignature = "0x3323c0a8383eff1662a638fc98dbb0f48a738aa7597e6811b5ab23c7580484447a7d17313818feb98ff43bddb086bc2e66056bdd6abf41f5d039c4e13e6cd4581b";assert (expectedSignature.equalsIgnoreCase(signature));}}package com.xxxx.nft.bcs; import lombok.extern.slf4j.Slf4j; import org.web3j.crypto.*; import org.web3j.utils.Numeric; import java.math.BigInteger; /** * @author keyang * @Description TODO **/ @Slf4j public class EIP712Utils { public static final byte[] EIP712DOMAIN_TYPEHASH = Hash.sha3("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)".getBytes()); public static final byte[] MessageTypeHash = Hash.sha3("swap(string sign_str)".getBytes()); // 生成domain hash public static String generateDomainSeparatorHash( String name, String version, Integer chainId, String verifyContractAddress ) { // 省略代码 } // 生成一个struct hash public static String generateStructHash(String signParams) { // 省略代码 } public static String signEIP712Message(String privateKey, String domainSeparatorHashHex, String structHashHex) { String messageHash = signHash(domainSeparatorHashHex, structHashHex); log.info("messageHash={}", messageHash); byte[] messageHashBytes = Numeric.hexStringToByteArray(messageHash); Credentials credentials = Credentials.create(privateKey); Sign.SignatureData signatureData = Sign.signMessage(messageHashBytes, credentials.getEcKeyPair(), false); String r = Numeric.toHexString(signatureData.getR()); String s = Numeric.toHexString(signatureData.getS()); BigInteger v = Numeric.toBigInt(signatureData.getV()); log.info("R={}", r); log.info("S={}", s); log.info("V={}", v); // 拼接签名 String signature = Numeric.toHexString(signatureData.getR()) + Numeric.toHexStringNoPrefix(signatureData.getS()) + Numeric.toHexStringNoPrefix(signatureData.getV()); return signature; } // done private static String signHash(String domainSeparatorHashHex, String structHashHex) { byte[] buffer1 = Numeric.hexStringToByteArray(domainSeparatorHashHex); // Generate or provide buffer1 byte[] buffer2 = Numeric.hexStringToByteArray(structHashHex); // Generate or provide buffer2 byte[] concatenatedHash = Hash.sha3(concatenate(Numeric.hexStringToByteArray("1901"), buffer1, buffer2)); return Numeric.toHexString(concatenatedHash); } // done private static byte[] concatenate(byte[]... arrays) { int totalLength = 0; for (byte[] array : arrays) { totalLength += array.length; } byte[] result = new byte[totalLength]; int currentIndex = 0; for (byte[] array : arrays) { System.arraycopy(array, 0, result, currentIndex, array.length); currentIndex += array.length; } return result; } public static void main(String[] args) throws Exception { // 私钥 String privateKey1 = "0xc85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4"; Credentials credentials = Credentials.create(privateKey1); // 生成地址 String signerAddress = credentials.getAddress(); log.info("Address: " + signerAddress); // 生成domainHash String domainSeparatorHash = generateDomainSeparatorHash("Nft Market", "1.0", 97, "0xCE83De204A9374CCA6b4c848565EBf0eEec85655"); log.info("domainSeparatorHash:{}", domainSeparatorHash); String expectedDomainSeparatorHash = "0xd82ca19e5ab4788d66937bc55d3ba6262acfed0f62bb35e2cf5abde9d4c2929b"; assert (expectedDomainSeparatorHash.equalsIgnoreCase(domainSeparatorHash)); // struct hash String signParam = "0xca293EB20BFbb9A2cBde3EC4878cFaE06059f1aD|2|1000000000000000000|0x194852a4Fd96C57f7Ba1D35b3e711D0cFd93a977|1|0x2609841E44B5F418a9Ed2062dFe561BC430A5A43|1681280890|1691280890|asdf|1|97"; String structHash = generateStructHash(signParam); log.info("structHash:{}", structHash); String exceptedStructHash = "0xc7e55c91bec99e28a920df095ac32e3fb2201523e18e9b862e9ff0d81307fbcc"; assert (exceptedStructHash.equalsIgnoreCase(structHash)); String signature = signEIP712Message(privateKey1, domainSeparatorHash, structHash); log.info("signature: " + signature); String expectedSignature = "0x3323c0a8383eff1662a638fc98dbb0f48a738aa7597e6811b5ab23c7580484447a7d17313818feb98ff43bddb086bc2e66056bdd6abf41f5d039c4e13e6cd4581b"; assert (expectedSignature.equalsIgnoreCase(signature)); } }package com.xxxx.nft.bcs; import lombok.extern.slf4j.Slf4j; import org.web3j.crypto.*; import org.web3j.utils.Numeric; import java.math.BigInteger; /** * @author keyang * @Description TODO **/ @Slf4j public class EIP712Utils { public static final byte[] EIP712DOMAIN_TYPEHASH = Hash.sha3("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)".getBytes()); public static final byte[] MessageTypeHash = Hash.sha3("swap(string sign_str)".getBytes()); // 生成domain hash public static String generateDomainSeparatorHash( String name, String version, Integer chainId, String verifyContractAddress ) { // 省略代码 } // 生成一个struct hash public static String generateStructHash(String signParams) { // 省略代码 } public static String signEIP712Message(String privateKey, String domainSeparatorHashHex, String structHashHex) { String messageHash = signHash(domainSeparatorHashHex, structHashHex); log.info("messageHash={}", messageHash); byte[] messageHashBytes = Numeric.hexStringToByteArray(messageHash); Credentials credentials = Credentials.create(privateKey); Sign.SignatureData signatureData = Sign.signMessage(messageHashBytes, credentials.getEcKeyPair(), false); String r = Numeric.toHexString(signatureData.getR()); String s = Numeric.toHexString(signatureData.getS()); BigInteger v = Numeric.toBigInt(signatureData.getV()); log.info("R={}", r); log.info("S={}", s); log.info("V={}", v); // 拼接签名 String signature = Numeric.toHexString(signatureData.getR()) + Numeric.toHexStringNoPrefix(signatureData.getS()) + Numeric.toHexStringNoPrefix(signatureData.getV()); return signature; } // done private static String signHash(String domainSeparatorHashHex, String structHashHex) { byte[] buffer1 = Numeric.hexStringToByteArray(domainSeparatorHashHex); // Generate or provide buffer1 byte[] buffer2 = Numeric.hexStringToByteArray(structHashHex); // Generate or provide buffer2 byte[] concatenatedHash = Hash.sha3(concatenate(Numeric.hexStringToByteArray("1901"), buffer1, buffer2)); return Numeric.toHexString(concatenatedHash); } // done private static byte[] concatenate(byte[]... arrays) { int totalLength = 0; for (byte[] array : arrays) { totalLength += array.length; } byte[] result = new byte[totalLength]; int currentIndex = 0; for (byte[] array : arrays) { System.arraycopy(array, 0, result, currentIndex, array.length); currentIndex += array.length; } return result; } public static void main(String[] args) throws Exception { // 私钥 String privateKey1 = "0xc85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4"; Credentials credentials = Credentials.create(privateKey1); // 生成地址 String signerAddress = credentials.getAddress(); log.info("Address: " + signerAddress); // 生成domainHash String domainSeparatorHash = generateDomainSeparatorHash("Nft Market", "1.0", 97, "0xCE83De204A9374CCA6b4c848565EBf0eEec85655"); log.info("domainSeparatorHash:{}", domainSeparatorHash); String expectedDomainSeparatorHash = "0xd82ca19e5ab4788d66937bc55d3ba6262acfed0f62bb35e2cf5abde9d4c2929b"; assert (expectedDomainSeparatorHash.equalsIgnoreCase(domainSeparatorHash)); // struct hash String signParam = "0xca293EB20BFbb9A2cBde3EC4878cFaE06059f1aD|2|1000000000000000000|0x194852a4Fd96C57f7Ba1D35b3e711D0cFd93a977|1|0x2609841E44B5F418a9Ed2062dFe561BC430A5A43|1681280890|1691280890|asdf|1|97"; String structHash = generateStructHash(signParam); log.info("structHash:{}", structHash); String exceptedStructHash = "0xc7e55c91bec99e28a920df095ac32e3fb2201523e18e9b862e9ff0d81307fbcc"; assert (exceptedStructHash.equalsIgnoreCase(structHash)); String signature = signEIP712Message(privateKey1, domainSeparatorHash, structHash); log.info("signature: " + signature); String expectedSignature = "0x3323c0a8383eff1662a638fc98dbb0f48a738aa7597e6811b5ab23c7580484447a7d17313818feb98ff43bddb086bc2e66056bdd6abf41f5d039c4e13e6cd4581b"; assert (expectedSignature.equalsIgnoreCase(signature)); } }
domainSeparator怎么生成?
public static String generateDomainSeparatorHash(String name, String version, Integer chainId, String verifyContractAddress) {// domain hashString cleanedAddress = Numeric.cleanHexPrefix(verifyContractAddress);byte[] addressBytes = Numeric.toBytesPadded(Numeric.toBigInt(cleanedAddress), 32);String encodedData = Numeric.toHexString(EIP712DOMAIN_TYPEHASH)+ Numeric.toHexStringNoPrefix(Hash.sha3(name.getBytes()))+ Numeric.toHexStringNoPrefix(Hash.sha3(version.getBytes()))+ Numeric.toHexStringNoPrefix(Numeric.toBytesPadded(BigInteger.valueOf(chainId), 32))+ Numeric.toHexStringNoPrefix(addressBytes);log.info("encodedData = {}", encodedData);return Numeric.toHexString(Hash.sha3(Numeric.hexStringToByteArray(encodedData)));}public static String generateDomainSeparatorHash( String name, String version, Integer chainId, String verifyContractAddress ) { // domain hash String cleanedAddress = Numeric.cleanHexPrefix(verifyContractAddress); byte[] addressBytes = Numeric.toBytesPadded(Numeric.toBigInt(cleanedAddress), 32); String encodedData = Numeric.toHexString(EIP712DOMAIN_TYPEHASH) + Numeric.toHexStringNoPrefix(Hash.sha3(name.getBytes())) + Numeric.toHexStringNoPrefix(Hash.sha3(version.getBytes())) + Numeric.toHexStringNoPrefix(Numeric.toBytesPadded(BigInteger.valueOf(chainId), 32)) + Numeric.toHexStringNoPrefix(addressBytes); log.info("encodedData = {}", encodedData); return Numeric.toHexString(Hash.sha3(Numeric.hexStringToByteArray(encodedData))); }public static String generateDomainSeparatorHash( String name, String version, Integer chainId, String verifyContractAddress ) { // domain hash String cleanedAddress = Numeric.cleanHexPrefix(verifyContractAddress); byte[] addressBytes = Numeric.toBytesPadded(Numeric.toBigInt(cleanedAddress), 32); String encodedData = Numeric.toHexString(EIP712DOMAIN_TYPEHASH) + Numeric.toHexStringNoPrefix(Hash.sha3(name.getBytes())) + Numeric.toHexStringNoPrefix(Hash.sha3(version.getBytes())) + Numeric.toHexStringNoPrefix(Numeric.toBytesPadded(BigInteger.valueOf(chainId), 32)) + Numeric.toHexStringNoPrefix(addressBytes); log.info("encodedData = {}", encodedData); return Numeric.toHexString(Hash.sha3(Numeric.hexStringToByteArray(encodedData))); }
structHash怎么生成?
public static String generateStructHash(String signParams) {String encodedData = Numeric.toHexStringNoPrefix(MessageTypeHash) +Numeric.toHexStringNoPrefix(Hash.sha3(signParams.getBytes()));log.info("encodedData = {}", encodedData);return Numeric.toHexString(Hash.sha3(Numeric.hexStringToByteArray(encodedData)));}public static String generateStructHash(String signParams) { String encodedData = Numeric.toHexStringNoPrefix(MessageTypeHash) + Numeric.toHexStringNoPrefix(Hash.sha3(signParams.getBytes())); log.info("encodedData = {}", encodedData); return Numeric.toHexString(Hash.sha3(Numeric.hexStringToByteArray(encodedData))); }public static String generateStructHash(String signParams) { String encodedData = Numeric.toHexStringNoPrefix(MessageTypeHash) + Numeric.toHexStringNoPrefix(Hash.sha3(signParams.getBytes())); log.info("encodedData = {}", encodedData); return Numeric.toHexString(Hash.sha3(Numeric.hexStringToByteArray(encodedData))); }
五、总结
1、其实不管是js、还是java、还是solidity,eth_signTypedData
的算法其实就做了这么一个操作:
sign(keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message)))sign(keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message)))sign(keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message)))
- sign代表签名,需要用到私钥
Keccak-256
就是计算hash,Web3j的Hash.sha3()
方法可以来计算Keccak-256哈希。- 然后就是将message和domain的hash分别计算出来,最后拼接在一起。
2、Numeric方法
Numeric.toHexString()
方法将哈希值转换为十六进制字符串
Numeric.toHexStringNoPrefix()
方法也一样,只不过没有0x
这个前缀;
Numeric.toBytesPadded()
是 web3j 库中的一个方法,用于将数值类型(如 BigInteger
)转换为字节数组,并添加填充字节以对齐数据。与 Solidity 中的 abi.encode
类似。
3、为什么要用abi.encodePacked? 比如在合约里,经常会看见这样子的代码:
function getEthSignedMessageHash(bytes32 _messageHash) public pure returns (bytes32) {/*Signature is produced by signing a keccak256 hash with the following format:"\x19Ethereum Signed Message\n" + len(msg) + msg*/return keccak256(abi.encodePacked('\x19Ethereum Signed Message:\n32', _messageHash));}function getEthSignedMessageHash(bytes32 _messageHash) public pure returns (bytes32) { /* Signature is produced by signing a keccak256 hash with the following format: "\x19Ethereum Signed Message\n" + len(msg) + msg */ return keccak256(abi.encodePacked('\x19Ethereum Signed Message:\n32', _messageHash)); }function getEthSignedMessageHash(bytes32 _messageHash) public pure returns (bytes32) { /* Signature is produced by signing a keccak256 hash with the following format: "\x19Ethereum Signed Message\n" + len(msg) + msg */ return keccak256(abi.encodePacked('\x19Ethereum Signed Message:\n32', _messageHash)); }
通过使用 abi.encodePacked
,我们可以避免填充字节的引入。abi.encodePacked
将参数紧密地打包在一起,没有额外的填充字节。这意味着在计算哈希时,只有参数的实际字节值参与到哈希的计算中,没有额外的干扰。
例如,考虑以下 Solidity 代码片段:
bytes32 hash = keccak256(abi.encodePacked(value1, value2));bytes32 hash = keccak256(abi.encodePacked(value1, value2));bytes32 hash = keccak256(abi.encodePacked(value1, value2));
如果我们直接使用 keccak256(value1, value2)
,那么由于填充字节的存在,最终的哈希值可能会与使用 abi.encodePacked
时的结果不一致。
因此,在使用 keccak256
计算哈希时,为了确保结果的一致性,我们经常使用 abi.encodePacked
来紧密打包参数。
4、 abi.encode
和 abi.encodePacked
又有什么不同?
bytes memory packed = abi.encodePacked(value1, value2);bytes memory encoded = abi.encode(value1, value2);bytes memory packed = abi.encodePacked(value1, value2); bytes memory encoded = abi.encode(value1, value2);bytes memory packed = abi.encodePacked(value1, value2); bytes memory encoded = abi.encode(value1, value2);
abi.encode
函数会根据参数的类型添加适当的填充字节,以确保对齐数据。与外部合约的交互中,当你需要确保参数在字节序列中的位置和长度是固定的。填充字节可以用于保留参数之间的间隔,以便接收方正确解析数据。
它的填充规则如下:
下面是
abi.encode
中常见参数类型的填充规则:
- 布尔类型 (
bool
):布尔类型不需要填充字节,因为它只占用一个字节。- 整数类型 (
int
,uint
):整数类型的填充是根据类型的大小来决定的。例如,uint8
类型需要占用一个字节,因此不需要填充字节,而uint256
类型需要占用 32 字节,因此可能会添加相应数量的填充字节以对齐数据。- 动态字节数组类型 (
bytes
):动态字节数组类型的编码包括长度和实际的字节数组。因此,不需要填充字节。- 固定大小字节数组类型 (
bytesN
):固定大小字节数组类型会根据数组的大小来添加填充字节。例如,如果是bytes32
类型,则不需要填充字节,因为它已经是 32 字节。但是,如果是bytes20
类型,则可能会添加 12 字节的填充字节,以对齐到 32 字节。- 固定大小的数组类型 (
type[]
):固定大小的数组类型会根据数组的大小和元素类型的填充规则来添加填充字节。例如,如果是uint256[2]
类型的数组,其中每个元素占用 32 字节,则可能会添加 28 字节的填充字节。
abi.encodePacked
将参数紧密地打包在一起,没有额外的填充字节。
所以我们在java代码处理地址类型数据的时候,要填充32。
String cleanedAddress = Numeric.cleanHexPrefix(verifyContractAddress);byte[] addressBytes = Numeric.toBytesPadded(Numeric.toBigInt(cleanedAddress), 32); // 有填充String cleanedAddress = Numeric.cleanHexPrefix(verifyContractAddress); byte[] addressBytes = Numeric.toBytesPadded(Numeric.toBigInt(cleanedAddress), 32); // 有填充String cleanedAddress = Numeric.cleanHexPrefix(verifyContractAddress); byte[] addressBytes = Numeric.toBytesPadded(Numeric.toBigInt(cleanedAddress), 32); // 有填充
为什么0xce83de204a9374cca6b4c848565ebf0eeec85655
要填充成000000000000000000000000ce83de204a9374cca6b4c848565ebf0eeec85655
因为0xce83de204a9374cca6b4c848565ebf0eeec85655
是一个 20 字节的地址(address
)类型。然而,当在 Solidity 的编码中使用固定大小的字节数组时,需要将其填充为固定长度。
在 bytes32
类型的情况下,长度为 32 字节,因此会在左侧填充零字节,使其达到固定长度。在你的示例中,需要将地址填充为 32 字节,因此会在左侧添加 12 个零字节,形成 000000000000000000000000ce83de204a9374cca6b4c848565ebf0eeec85655
。
参考文档
metamask的签名方法: docs.metamask.io/guide/signi…
Test Dapp: github.com/MetaMask/te…
eip-712/Example.js: github.com/ethereum/EI…
eth-sig-util: github.com/MetaMask/et…