一、怎么签名以及验证签名?

签名

  1. 首要即将签名的message拼接起来
  2. 对message进行hash运算
  3. 将hash成果进行签名(在链下签名,需求用到私钥,一般都是需求调用钱包插件来进行签名);

验证签名

  1. 对message重新进行hash运算;
  2. 经过签名和hash,来反算出signer;这一步首要用到了ecrecover办法
  3. 比较signer是不是共同的

二、签名有哪些类型

Metamask供给了几种不同的签名办法,整个历史改变如下:

1、eth_sign==(风险)==

能够签名恣意数据,可是很风险,现已不引荐运用。metamask也会有风险提示:

【Dapp开发】Dapp里关于签名和验证签名

2、personal_sign

该办法在任何签名数据前都会加上\x19Ethereum Signed Message:\n

这意味着假如有人要签署交易数据,增加的前缀字符串会使其成为无效交易。

【Dapp开发】Dapp里关于签名和验证签名

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的办法,对字符串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: 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;
//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);

在web3js里能够这么写

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);
        }

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#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);

假如想尝试,能够用matamask供给的eth_signTypedData办法来唤起。效果如下:

【Dapp开发】Dapp里关于签名和验证签名

经过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());
  });

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());
  });

合约代码:

// 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());
  });

合约代码:

// 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 });
// 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); 

终究完成

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 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)));
    }

五、总结

1、其实不管是js、还是java、还是solidity,eth_signTypedData的算法其实就做了这么一个操作:

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));
    }

经过运用 abi.encodePacked,咱们能够避免填充字节的引入。abi.encodePacked 将参数紧密地打包在一起,没有额定的填充字节。这意味着在核算哈希时,只要参数的实践字节值参与到哈希的核算中,没有额定的搅扰。

例如,考虑以下 Solidity 代码片段:

bytes32 hash = keccak256(abi.encodePacked(value1, value2));

假如咱们直接运用 keccak256(value1, value2),那么由于填充字节的存在,终究的哈希值或许会与运用 abi.encodePacked 时的成果不共同。

因而,在运用 keccak256 核算哈希时,为了保证成果的共同性,咱们常常运用 abi.encodePacked 来紧密打包参数。

4、 abi.encodeabi.encodePacked又有什么不同?

bytes memory packed = abi.encodePacked(value1, value2);
bytes memory encoded = abi.encode(value1, value2);

abi.encode函数会依据参数的类型增加适当的填充字节,以保证对齐数据。与外部合约的交互中,当你需求保证参数在字节序列中的方位和长度是固定的。填充字节能够用于保留参数之间的距离,以便接收方正确解析数据。

它的填充规矩如下:

下面是 abi.encode 中常见参数类型的填充规矩:

  1. 布尔类型 (bool):布尔类型不需求填充字节,由于它只占用一个字节。
  2. 整数类型 (int, uint):整数类型的填充是依据类型的巨细来决议的。例如,uint8 类型需求占用一个字节,因而不需求填充字节,而 uint256 类型需求占用 32 字节,因而或许会增加相应数量的填充字节以对齐数据。
  3. 动态字节数组类型 (bytes):动态字节数组类型的编码包含长度和实践的字节数组。因而,不需求填充字节。
  4. 固定巨细字节数组类型 (bytesN):固定巨细字节数组类型会依据数组的巨细来增加填充字节。例如,假如是 bytes32 类型,则不需求填充字节,由于它现已是 32 字节。可是,假如是 bytes20 类型,则或许会增加 12 字节的填充字节,以对齐到 32 字节。
  5. 固定巨细的数组类型 (type[]):固定巨细的数组类型会依据数组的巨细和元素类型的填充规矩来增加填充字节。例如,假如是 uint256[2] 类型的数组,其间每个元素占用 32 字节,则或许会增加 28 字节的填充字节。

abi.encodePacked 将参数紧密地打包在一起,没有额定的填充字节。

所以咱们在java代码处理地址类型数据的时候,要填充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…

签名文章:medium.com/metamask/ei…

eip-712/Example.js: github.com/ethereum/EI…

eth-sig-util: github.com/MetaMask/et…