ethereum / EIPs

The Ethereum Improvement Proposal repository

Home Page:https://eips.ethereum.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Bug]: Failed to Verify Signs by Multiple Structured Data Nested through EIP712

VictorZhang2014 opened this issue · comments

Pull Request

No response

What happened?

Hi Ethereum dev team, I came across a bug inside solidity abi.encode/abi.encodePacked. The question is that I nested over 3 struct data via EIP712 structure, whatever I encoded the structs data, it failed every time.

I just used the Ether Mail solidity code and its related javascript code. Like I took a screenshot of the structured data, Mail and Person are just two nested struct data, and I added one more struct called HomeAddress to the Person struct, it cannot pass the test.

Ether-Mail-Screenshot

This is my Solidity code snippet:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

contract EIP712EtherMail {
    
    struct EIP712Domain {
        string  name;
        string  version;
        uint256 chainId;
        address verifyingContract;
    }

    struct HomeAddress {
        string home;
        string phone;
        uint256 age;
    }

    struct Person {
        string name;
        address wallet;
        HomeAddress addr;
    }

    struct Mail {
        Person from;
        Person to;
        string contents;
    }

    bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
        "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
    );

    bytes32 constant HOME_ADDRESS_TYPEHASH = keccak256(
        "HomeAddress(string home,string phone,uint256 age)"
    );

    bytes32 constant PERSON_TYPEHASH = keccak256(
        "Person(string name,address wallet,HomeAddress addr)HomeAddress(string home,string phone,uint256 age)"
    );

    bytes32 constant MAIL_TYPEHASH = keccak256(
        "Mail(Person from,Person to,string contents)Person(string name,address wallet,HomeAddress addr)HomeAddress(string home,string phone,uint256 age)"
    );

    bytes32 DOMAIN_SEPARATOR;

    constructor () {
        DOMAIN_SEPARATOR = hash(EIP712Domain({
            name: "Ether Mail",
            version: '1',
            chainId: block.chainid,
            // verifyingContract: this
            verifyingContract: 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC
        }));
    }

    function hash(EIP712Domain memory eip712Domain) public pure returns (bytes32) {
        return keccak256(abi.encode(
            EIP712DOMAIN_TYPEHASH,
            keccak256(bytes(eip712Domain.name)),
            keccak256(bytes(eip712Domain.version)),
            eip712Domain.chainId,
            eip712Domain.verifyingContract
        ));
    }

    function hash(HomeAddress memory addr) public pure returns (bytes32) {
        return keccak256(abi.encode(
            HOME_ADDRESS_TYPEHASH,
            keccak256(bytes(addr.home)),
            keccak256(bytes(addr.phone)),
            addr.age
        ));
    }

    function hash(Person memory person) public pure returns (bytes32) {
        return keccak256(abi.encode(
            PERSON_TYPEHASH,
            keccak256(bytes(person.name)),
            person.wallet,
            hash(person.addr)
        ));
    }

    function hash(Mail memory mail) public pure returns (bytes32) {
        return keccak256(abi.encode(
            MAIL_TYPEHASH,
            hash(mail.from),
            hash(mail.to),
            keccak256(bytes(mail.contents))
        ));
    }

    function verify(Mail memory mail, uint8 v, bytes32 r, bytes32 s) public view returns (address) {
        // Note: we need to use `encodePacked` here instead of `encode`.
        bytes32 digest = keccak256(abi.encodePacked(
            "\x19\x01",
            DOMAIN_SEPARATOR,
            hash(mail)
        ));
        return ecrecover(digest, v, r, s);
    }
}

Attached to my javascript code snippet:

import Web3 from "web3";
const EthUtil = require('ethereumjs-util');

import EncodeDataUtil from "./EncodeDataUtil"
const EIP712_MAIL_EXAMPLE_ABI = require("../assets/abi/eip712mail.json")


    let chainId = window.ethereum.chainId
    if (chainId == null || chainId == undefined) {
        chainId = 31337
    }

    this.typedData = {
        types: {
            EIP712Domain: [
                { name: 'name', type: 'string' },
                { name: 'version', type: 'string' },
                { name: 'chainId', type: 'uint256' },
                { name: 'verifyingContract', type: 'address' },
            ],
            HomeAddress: [
                { name: 'home', type: 'string' },
                { name: 'phone', type: 'string' },
                { name: 'age', type: 'uint256' },
            ],
            Person: [
                { name: 'name', type: 'string' },
                { name: 'wallet', type: 'address' },
                { name: 'addr', type: 'HomeAddress' }
            ],
            Mail: [
                { name: 'from', type: 'Person' },
                { name: 'to', type: 'Person' },
                { name: 'contents', type: 'string' }
            ]
        },
        primaryType: 'Mail',
        domain: {
            name: 'Ether Mail',
            version: '1',
            chainId: chainId,
            verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
        },
        message: {
            from: {
                name: 'Cow',
                wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
                addr: {
                    home: "The USA",
                    phone: "123456",
                    age: 20
                }
            },
            to: {
                name: 'Bob',
                wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
                addr: {
                    home: "United Kindom",
                    phone: "098765",
                    age: 22
                }
            },
            contents: 'Hello, Bob!',
        },
    }


    // this.$privateKey = EthUtil.keccakFromString('cow', 256); // to generate a new private key
    const _privateKey = "Your private key to paste it here"
    this.privateKey = EthUtil.toBuffer(_privateKey)
    this.from = EthUtil.bufferToHex(EthUtil.privateToAddress(this.privateKey));
    console.log("from=", this.from) 




const encodeUtil = new EncodeDataUtil(this.typedData.types)
const msgHash = encodeUtil.signHash(this.typedData.domain, this.typedData.primaryType, this.typedData.message)

const hexMsgHash = JSON.stringify(this.typedData)
const signature_ = window.ethereum.request({ method: 'eth_signTypedData_v4', params: [this.from, hexMsgHash]})
const sig = EthUtil.fromRpcSig(signature_)


const web3 = new Web3("http://localhost:8545")
const contractAddress = "0x1291Be112d480055DaFd8a610b7d1e203891C274"
const myContract = new web3.eth.Contract(EIP712_MAIL_EXAMPLE_ABI, contractAddress);

const mail = [
        ['Cow', '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', ["The USA","123456",20]],
        ['Bob', '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', ["United Kindom","098765",22]],
        "Hello, Bob!"
]

const result = await myContract.methods.verify(mail, this.signature.v, this.signature.r, this.signature.s).call();
console.log("1. The signer address is ", result)
console.log(result.toLowerCase() == this.from.toLowerCase() ? "1. Verifying Success!" : "Failed to verify")

const sign_ = EthUtil.toRpcSig(this.signature.v, this.signature.r, this.signature.s)
const result1 = await myContract.methods.verifySignature(mail, sign_).call();
console.log("2. The signer address is ", result1)
console.log(result1.toLowerCase() == this.from.toLowerCase() ? "2. Verifying Success!" : "Failed to verify")

The source code of object EncodeDataUtil is derived from the linke https://github.com/ethereum/EIPs/blob/master/assets/eip-712/Example.js

Relevant log output

To output the recover public key that is equal to the signer address

This is the wrong repo. Please open an issue at https://github.com/ethereum/solidity/issues

I run into similar issue and I've figure it's probably best for the EIP-712 author to provide a good implementation reference. #5433

EIP-712 was just finalized. It is too late for that.

@xinbenlv Yep, Aug. 8 2022 was the Last Call and Finalized. But I think the solidty code snippets is same as that examples, doesn't it? Or so what is the big change to the solidity code?

The puzzle has solved by sorting the name list of these multiple structs. See the attachement.

Solidity Three Structs Nested - EIP712