Ethers.js

驗證 EIP-712 時恢復的帳戶不一致(javascript / python)

  • October 16, 2022

我有一個簽署 EIP-712 消息的客戶端:

eip_712.js``ethers: 5.7.1(部:)

const { Wallet } = require("@ethersproject/wallet");
const { _TypedDataEncoder } = require("@ethersproject/hash");

console.log("EIP-712 JS");

// ! this is a test private key, DO NOT USE IT
const privKey = '0x0123456789012345678901234567890123456789012345678901234567890123';
const signer = new Wallet(privKey);
console.log("Signer is: ", signer.address);

// ! You need to fix your ethers version because _signTypedData is experimental
// ! feature.
// * https://docs.ethers.io/v5/api/signer/#Signer-signTypedData

const domain = {
   chainId: 0,
   name: '',
   verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
   version: ''
};

// The named list of all type definitions
const types = {
   Player: [
       { name: 'wallet', type: 'address' },
       { name: 'email', type: 'string' }
   ],
   Message: [
       { name: 'player', type: 'Player' },
   ]
};

// The data to sign
const value = {
   player: { 
       wallet: "0x14791697260E4c9A71f18484C9f997B308e59325",
       email: "steve@jobs.com",
   }
};

console.log(
   "Signing this digest: ", _TypedDataEncoder.hash(domain, types, value)
);

signer._signTypedData(
   domain,
   types,
   value
).then(signature => {
   console.log("Signature is: ", signature);
}).catch(e => {
   console.log("Something went wrong", e);
});

現在我正在嘗試驗證 python 中的 from 帳戶:

eip_712.py(部門:eip712 = "0.1.4"eth-account = "0.7.0"

#!/usr/bin/env python
from eip712.messages import EIP712Message, EIP712Type
from eth_account import Account

print("EIP-712 PY")

class Player(EIP712Type):
   wallet: "address"
   email: "string"


class Message(EIP712Message):
   _chainId_: "uint256" = 0
   _name_: "string" = ''
   _verifyingContract_: "address" = '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC'
   _version_: "string" = '' 

   player: Player 

signature = bytes.fromhex('f65e247fc142c6fa98febae4dba127be1694e83a5fce938c4389945594a5acd454771cdf810b90f2f83d60c69f8d576d5ff8dab957f57b2a247c86baffae6d011c')

message = Message(player=Player(
   wallet='0x14791697260E4c9A71f18484C9f997B308e59325',
   email='steve@jobs.com'
))


# this is the digest that the user signs on the client side
print('Digest that was signed: ', message.body.hex())

from_account = Account.recover_message(message.signable_message, signature=signature)
print('Account that signed the message: ', from_account)

以上兩個的輸出是:

EIP-712 JS
Signer is:  0x14791697260E4c9A71f18484C9f997B308e59325
Signing this digest:  0x044b3a930b5d539e1ec6f5ab7de69a1c6c0f103a11c6377adbd81e92763df9be
Signature is:  0xf65e247fc142c6fa98febae4dba127be1694e83a5fce938c4389945594a5acd454771cdf810b90f2f83d60c69f8d576d5ff8dab957f57b2a247c86baffae6d011c

EIP-712 PY
Digest that was signed:  e5477989520456a98eb64ab80304b4e9b1858fe542121f4722a0ea1e12aed8c9
Account that signed the message:  0xE01Fc9697648F725409D75B08f5e7D98CddD13bD

看起來這兩種方法簽名的摘要不同?

編輯:Python 端的摘要實際上並不是用於恢復帳戶的摘要。我進入eth_accountlib 並列印了實際的摘要。這是文件message_hash中的第 450 行account.py。實際使用的摘要是:0xa7875cf3863821279f6b8d2c254e34caec0e7376cb37fda6387150b0ec3aaf96. 這意味著eth_account正在使用與ethers.js生成的摘要不同的摘要。我會將摘要硬編碼ethers.js到其中eth_account以驗證摘要是否確實正確,它是否會準確地恢復地址。

編輯:確實,如果我使用來自 的摘要ethers.js,那麼eth_account正在恢復正確的簽名者地址。這意味著如果我們弄清楚為什麼eth_account會產生不同的摘要,那麼我們就在做生意。

編輯:我已經驗證,不使用eip712庫而直接對結構化數據進行編碼,會在 Python 端產生完全相同的錯誤摘要。即以下也是無效的:

signable_message = eth_account.messages.encode_structured_data(
       {
           "message": {
               "player": {
                   "wallet": "0x14791697260E4c9A71f18484C9f997B308e59325",
                   "email": "steve@jobs.com"
               }
           },
           "types": {
               "EIP712Domain": [
                  {"name": "chainId", "type": "uint256"},
                  {"name": "name", "type": "string"},
                  {"name": "verifyingContract", "type": "address"},
                  {"name": "version", "type": "string"}
               ],
               "Player": [
                   {"name": "wallet", "type": "address"},
                   {"name": "email", "type": "string"}
               ],
               "Message": [
                   {"name": "player", "type": "Player"}
               ]
           },
           "primaryType": "Message",
           "domain": {
               "chainId": 0,
               "name": "",
               "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
               "version": ""
           }
       }
   )

編輯:我注意到編碼消息標題的差異。Ethers.js 將域編碼為:0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cccccccccccccccccccccccccccccccccccccccc,而eip712(和eth_account)將其編碼為:0x03fdd899fb21de70ced90f32a1783b1f2014b8b662f8233df477aeefb0ff0e4a0000000000000000000000000000000000000000000000000000000000000000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470000000000000000000000000ccccccccccccccccccccccccccccccccccccccccc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470

以上似乎是編碼順序與對主要類型進行不同編碼的差異。

編輯:注意到消息標頭編碼的差異,並註意到只需要 EIP712 域中的一個欄位,我設法使用標頭中的單個值實現相同的編碼。即,我只使用,而不是使用所有的 : chainId、和,這在編碼中實現了奇偶校驗。因此,這表明要解決兩者之間的差異,必須進行一些排序。無論是側面還是側面。Given已經進行了欄位名稱排序,並且由於它在客戶端更廣泛地使用,因此在 Python 端修復排序是有意義的。verifyingContract``name``version``name``ether.js``python``ethers.js

編輯:編碼和擁有身體似乎沒有問題,它們匹配。因此,我可以在 Python 端檢索正確的簽名者。

編輯:我製作了一個複制上述所有內容的倉庫,您可以在此送出中找到它:https ://github.com/nazariyv/eip712-discrepancy/commit/c7baa4630cbb5f1d01b5f123ea0727d527fb25c4

您無法檢索到正確簽名者的原因是 EIP712Domain 編碼的差異。似乎因為ethers.js對類型進行排序,但 Python 沒有,所以你得到了不同的編碼頭,這會導致問題。消息的實際值沒有問題,至少在這個例子中沒有(那裡可能仍然存在問題)。

這不是一個真正的解決方案,但對我來說就足夠了:刪除 EIP712Domain 上的所有欄位,只使用名稱。

為了eip_712.js

const domain = {
   //chainId: 0,
   name: 'RKL Player Data',
   //verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
   //version: ''
};

為了eip_712.py

class Message(EIP712Message):
   _name_: "string" = "RKL Player Data"
   # _chainId_: "uint256" = 0  # noqa
   # _name_: "string" = ""  # noqa
   # _verifyingContract_: "address" = (  # noqa
   #     "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
   # )
   # _version_: "string" = ""  # noqa

   player: Player

並且您將在消息中實現奇偶校驗,因此將能夠正確恢復簽名者。

編輯:確實,問題在於 Python 中標頭欄位的排序。如果您將排序更改為您在此處看到的,您可以在 Pyhton 端恢復發件人沒問題:https ://github.com/nazariyv/eip712-discrepancy/tree/fix

引用自:https://ethereum.stackexchange.com/questions/137491