EIP-712:類型化結構化數據的哈希與簽名

1. 引言

以太坊 EIP-712: 類型化結構化數據的哈希與簽名,是一種用于對類型化結構化數據(而不僅僅是字節串)進行哈希和簽名 的標準。

其包括:

  • 編碼函數正確性的理論框架,
  • 類似于 Solidity 結構體并兼容的結構化數據規范,
  • 對這些結構實例的安全哈希算法,
  • 將這些實例安全地包含在可簽名消息集合中的方法,
  • 一種可擴展的域分離機制,
  • 新的 RPC 調用 eth_signTypedData
  • EVM 中該哈希算法的優化實現。

該標準不包含重放保護機制。

2. 動機

如果只關注字節串,數據簽名已經是一個被解決的問題。不幸的是,在現實世界中,人們關心的是復雜且有意義的消息。對結構化數據進行哈希并非易事,錯誤可能會導致系統安全性的喪失。

因此,“don’t roll your own crypto 不要自己實現加密算法”這一原則適用。相反,需要使用經過同行評審和充分測試的標準方法。本 EIP 旨在成為這樣的標準。

本 EIP 旨在改善鏈下消息簽名在鏈上的可用性。鏈下消息簽名的采用正在增加,因為它節省了 Gas 并減少了區塊鏈上的交易數量。目前,已簽名的消息是一個不透明的十六進制字符串,用戶無法理解消息的組成內容。
在這里插入圖片描述

在此,概述了一種方案,以編碼數據及其結構,使其在簽名時可供用戶驗證。下面是根據本提案,用戶在簽署消息時可能會看到的示例界面。
在這里插入圖片描述

3. 規范

可簽名消息集合從交易和字節串 𝕋 ∪ 𝔹?? 擴展到包括結構化數據 𝕊。新的可簽名消息集合為 𝕋 ∪ 𝔹?? ∪ 𝕊。這些消息被編碼為適合哈希和簽名的字節串,如下所示:

  • encode(transaction : 𝕋) = RLP_encode(transaction)
  • encode(message : 𝔹??) = "\x19Ethereum Signed Message:\n" ‖ len(message) ‖ message
    其中 len(message)message 字節數的 非零填充 ASCII 十進制編碼。
  • encode(domainSeparator : 𝔹2??, message : 𝕊) = "\x19\x01" ‖ domainSeparator ‖ hashStruct(message)
    其中 domainSeparatorhashStruct(message) 定義如下。

此編碼是確定性的,因為其組成部分是確定性的。該編碼是單射的,因為三種情況的首字節總是不同。(RLP_encode(transaction) 不會以 \x19 開頭。)

此編碼符合 EIP-191 規范。其中,“版本字節” 固定為 0x01,“版本特定數據” 是 32 字節的域分隔符 domainSeparator,“要簽名的數據” 是 32 字節的 hashStruct(message)

3.1 類型化結構化數據 𝕊 的定義

為了定義所有結構化數據的集合,首先定義可接受的類型。類似于 ABIv2,這些類型與 Solidity 類型密切相關。采用 Solidity 語法有助于解釋定義。該標準特定于以太坊虛擬機(EVM),但旨在不依賴于更高級別的語言。如:

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

其中:

  • 定義:struct 結構體類型
    結構體類型的名稱是一個有效的標識符,包含零個或多個成員變量。成員變量具有成員類型和名稱。
  • 定義:member 成員類型
    成員類型可以是原子類型、動態類型或引用類型。
  • 定義:原子類型
    原子類型包括 bytes1bytes32uint8uint256int8int256booladdress。這些類型與 Solidity 定義相對應。需要注意的是,沒有 uintint 的別名。此外,合約地址始終是 address。本標準不支持定點數類型,未來版本可能會增加新的原子類型。
  • 定義:動態類型
    動態類型包括 bytesstring。這些類型在類型聲明方面類似于原子類型,但它們的編碼方式不同。
  • 定義:引用類型
    引用類型包括數組和結構體。數組可以是固定大小的 Type[n] 或動態大小的 Type[]。結構體是對其他結構體的引用,通過其名稱標識。本標準支持遞歸結構體類型。
  • 定義:結構化類型數據 𝕊
    𝕊 包含所有結構體類型的所有實例。

3.2 hashStruct 的定義

hashStruct 函數定義如下:

  • hashStruct(s : 𝕊) = keccak256(typeHash ‖ encodeData(s))
    其中 typeHash = keccak256(encodeType(typeOf(s)))

注意:對于給定的結構體類型,typeHash 是一個常量,無需在運行時計算。

3.3 encodeType 的定義

結構體類型編碼為 name ‖ "(" ‖ member? ‖ "," ‖ member? ‖ "," ‖ … ‖ member? ")",其中每個成員的表示形式為 type ‖ " " ‖ name

如,上述 Mail 結構體的編碼為 Mail(address from,address to,string contents)

如果結構體類型引用了其他結構體類型(這些結構體類型又進一步引用其他結構體類型),則收集所有引用的結構體類型,按名稱排序,并附加到編碼中。如:

Transaction(Person from,Person to,Asset tx)Asset(address token,uint256 amount)Person(address wallet,string name)

3.4 domainSeparator 的定義

domainSeparator = hashStruct(eip712Domain)

其中 eip712Domain 的類型是 EIP712Domain 結構體,包含以下字段之一或多個。這些字段用于區分不同的簽名域。未使用的字段不會包含在結構體類型中。

  • string name 簽名域的用戶可讀名稱,如 DApp 或協議的名稱。
  • string version 當前簽名域的主要版本。不同版本的簽名不兼容。
  • uint256 chainId EIP-155 的鏈 ID。
  • address verifyingContract 用于驗證簽名的合約地址。
  • bytes32 salt 作為協議的區分標識符。

未來的標準擴展可能會增加新的字段,用戶代理可據此提供更多安全措施或提示用戶。

3.5 eth_signTypedData JSON RPC 規范說明

eth_signTypedData 方法被添加到以太坊 JSON-RPC,類似于 eth_sign 方法。

3.5.1 eth_signTypedData

該簽名方法計算以太坊特定的簽名:
sign(keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message))),如上所定義。

注意:用于簽名的地址必須是解鎖狀態。

其中參數:

  • 1)Address - 20 字節 - 用于簽名消息的賬戶地址。
  • 2)TypedData - 需要簽名的結構化數據。

結構化數據是一個包含類型信息、域分隔符參數和消息對象的 JSON 對象。
以下是 TypedData 參數的 JSON Schema 定義:

{"type": "object","properties": {"types": {"type": "object","properties": {"EIP712Domain": {"type": "array"}},"additionalProperties": {"type": "array","items": {"type": "object","properties": {"name": {"type": "string"},"type": {"type": "string"}},"required": ["name", "type"]}},"required": ["EIP712Domain"]},"primaryType": {"type": "string"},"domain": {"type": "object"},"message": {"type": "object"}},"required": ["types", "primaryType", "domain", "message"]
}

返回值為:

  • 返回 DATA 類型,即簽名結果。
    eth_sign 方法相同,簽名是一個以 0x 開頭的 65 字節十六進制字符串,編碼了以太坊黃皮書附錄 F 中的 rsv 參數,采用大端字節序格式:
  • 字節 0-31:包含 r 參數
  • 字節 32-63:包含 s 參數
  • 最后一個字節:包含 v 參數

注意v 參數包括鏈 ID,符合 EIP-155 的規范。

示例:

  • 請求:

    curl -X POST --data '{"jsonrpc":"2.0","method":"eth_signTypedData","params":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", {"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"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":1,"verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},"message":{"from":{"name":"Cow","wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},"to":{"name":"Bob","wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"},"contents":"Hello, Bob!"}}],"id":1}'
    
  • 返回結果:

    {"id": 1,"jsonrpc": "2.0","result": "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c"
    }
    

一個示例展示如何使用 Solidity 的 ecrecover 來驗證 eth_signTypedData 計算出的簽名,代碼可參考 Example.js。
該合約已部署在 Ropsten 和 Rinkeby 測試網絡上。

// using ethereumjs-util 7.1.3
const ethUtil = require('ethereumjs-util');// using ethereumjs-abi 0.6.9
const abi = require('ethereumjs-abi');// using chai 4.3.4
const chai = require('chai');const typedData = {types: {EIP712Domain: [{ name: 'name', type: 'string' },{ name: 'version', type: 'string' },{ name: 'chainId', type: 'uint256' },{ name: 'verifyingContract', type: 'address' },],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: 1,verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',},message: {from: {name: 'Cow',wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',},to: {name: 'Bob',wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',},contents: 'Hello, Bob!',},
};const types = typedData.types;// Recursively finds all the dependencies of a type
function dependencies(primaryType, found = []) {if (found.includes(primaryType)) {return found;}if (types[primaryType] === undefined) {return found;}found.push(primaryType);for (let field of types[primaryType]) {for (let dep of dependencies(field.type, found)) {if (!found.includes(dep)) {found.push(dep);}}}return found;
}function encodeType(primaryType) {// Get dependencies primary first, then alphabeticallet deps = dependencies(primaryType);deps = deps.filter(t => t != primaryType);deps = [primaryType].concat(deps.sort());// Format as a string with fieldslet result = '';for (let type of deps) {result += `${type}(${types[type].map(({ name, type }) => `${type} ${name}`).join(',')})`;}return result;
}function typeHash(primaryType) {return ethUtil.keccakFromString(encodeType(primaryType), 256);
}function encodeData(primaryType, data) {let encTypes = [];let encValues = [];// Add typehashencTypes.push('bytes32');encValues.push(typeHash(primaryType));// Add field contentsfor (let field of types[primaryType]) {let value = data[field.name];if (field.type == 'string' || field.type == 'bytes') {encTypes.push('bytes32');value = ethUtil.keccakFromString(value, 256);encValues.push(value);} else if (types[field.type] !== undefined) {encTypes.push('bytes32');value = ethUtil.keccak256(encodeData(field.type, value));encValues.push(value);} else if (field.type.lastIndexOf(']') === field.type.length - 1) {throw 'TODO: Arrays currently unimplemented in encodeData';} else {encTypes.push(field.type);encValues.push(value);}}return abi.rawEncode(encTypes, encValues);
}function structHash(primaryType, data) {return ethUtil.keccak256(encodeData(primaryType, data));
}function signHash() {return ethUtil.keccak256(Buffer.concat([Buffer.from('1901', 'hex'),structHash('EIP712Domain', typedData.domain),structHash(typedData.primaryType, typedData.message),]),);
}const privateKey = ethUtil.keccakFromString('cow', 256);
const address = ethUtil.privateToAddress(privateKey);
const sig = ethUtil.ecsign(signHash(), privateKey);const expect = chai.expect;
expect(encodeType('Mail')).to.equal('Mail(Person from,Person to,string contents)Person(string name,address wallet)');
expect(ethUtil.bufferToHex(typeHash('Mail'))).to.equal('0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2',
);
expect(ethUtil.bufferToHex(encodeData(typedData.primaryType, typedData.message))).to.equal('0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8',
);
expect(ethUtil.bufferToHex(structHash(typedData.primaryType, typedData.message))).to.equal('0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e',
);
expect(ethUtil.bufferToHex(structHash('EIP712Domain', typedData.domain))).to.equal('0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f',
);
expect(ethUtil.bufferToHex(signHash())).to.equal('0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2');
expect(ethUtil.bufferToHex(address)).to.equal('0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826');
expect(sig.v).to.equal(28);
expect(ethUtil.bufferToHex(sig.r)).to.equal('0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d');
expect(ethUtil.bufferToHex(sig.s)).to.equal('0x07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b91562');

3.5.2 personal_signTypedData

還應有一個對應的 personal_signTypedData 方法,該方法接受賬戶的密碼作為最后一個參數。

3.6 Web3 API 規范

在 Web3.js 版本 1 中新增了兩個方法,與 web3.eth.signweb3.eth.personal.sign 方法對應。

3.6.1 web3.eth.signTypedData

web3.eth.signTypedData(typedData, address [, callback])

該方法使用特定賬戶簽名結構化數據,該賬戶需要是解鎖狀態。

其中參數:

  • 1)Object - 包含域分隔符和待簽名的結構化數據,結構遵循 eth_signTypedData JSON RPC 調用中指定的 JSON-Schema。
  • 2)String|Number - 用于簽名的數據的地址,或者本地錢包 web3.eth.accounts.wallet 中的地址或索引。
  • 3)Function (可選) - 可選回調函數,第一個參數返回錯誤對象,第二個參數返回簽名結果。

注意:參數 address 也可以是 web3.eth.accounts.wallet 中的地址或索引,此時會使用該賬戶的私鑰本地簽名。

返回值為:

  • Promise 返回 String 類型的簽名,與 eth_signTypedData 方法返回的結果相同。

示例:
可以參考 eth_signTypedData JSON-RPC 示例中的 typedData 值:

web3.eth.signTypedData(typedData, "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826")
.then(console.log);
> "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c"

3.6.2 web3.eth.personal.signTypedData

web3.eth.personal.signTypedData(typedData, address, password [, callback])

此方法與 web3.eth.signTypedData 基本相同,但額外增加了 password 參數,類似于 web3.eth.personal.sign

4. 設計原理(Rationale)

encode 函數針對新的類型擴展了新的處理方式,編碼的首字節用于區分不同的情況。
因此,直接使用 domainSeparatortypeHash 作為編碼的起始位置是不安全的。盡管構造一個 typeHash 作為有效 RLP 編碼交易的前綴很困難,但理論上仍然可能發生。

作用域分隔符(Domain Separator)的作用有:

  • 1)防止不同 DApp 之間的簽名沖突
    假設兩個 DApp 恰好設計了相同的結構,如 Transfer(address from,address to,uint256 amount),但它們的簽名不應該兼容。引入作用域分隔符后,DApp 開發者可以確保不會出現簽名沖突。

  • 2)允許同一 DApp 內部區分不同簽名用途
    在同一個 DApp 內,同一結構可能需要多種簽名。如,在 Transfer 交易中,可能既需要 from 簽名,也需要 to 簽名。通過提供不同的作用域分隔符,可以區分這兩種簽名。

方案 1:使用目標合約地址作為作用域分隔符

  • 這種方法可以解決合約間的類型沖突問題,但無法區分相同結構的不同簽名用途。因此,該標準建議在適當情況下使用目標合約地址。

4.1 typeHash 設計原理

typeHash 設計為 Solidity 編譯時的常量,如:

bytes32 constant MAIL_TYPEHASH = keccak256("Mail(address from,address to,string contents)");

對于 typeHash,曾考慮過以下幾種替代方案,但因各種原因被否決:

  • 方案 2:使用 ABIv2 函數簽名
    采用 bytes4 作為哈希值的長度不足以抵抗哈希碰撞。此外,與函數簽名不同,使用較長的哈希值幾乎不會增加運行時成本。
  • 方案 3:將 ABIv2 函數簽名擴展為 256 位
    這種方式雖然可以捕獲類型信息,但無法表達函數以外的語義。例如,在 EIP-20 和 EIP-721 中,transfer(address,uint256) 產生了實際碰撞:前者的 uint256 代表的是數量,而后者代表的是唯一 ID。總體而言,ABIv2 旨在增強兼容性,而哈希標準應優先考慮不可兼容性,以避免沖突。
  • 方案 4:將 256 位 ABIv2 簽名擴展為包含參數名和結構體名
    如,Mail 結構體可以被編碼為: Mail(Person(string name,address wallet) from,Person(string name,address wallet) to,string contents)
    但這種方案比現有的解決方案要長得多,并且字符串的長度可能會隨著輸入的增加呈指數級增長(如:struct A { B a; B b; }; struct B { C a; C b; }; …)。此外,該方案不支持遞歸結構體類型(如:struct List { uint256 value; List next; })。
  • 方案 5:包含 natspec 文檔
    這種方式在 schemaHash 中加入了更多的語義信息,進一步降低了哈希碰撞的可能性。然而,這會導致文檔的擴展和修改成為破壞性變更(breaking change),違背了通常的假設。此外,這也使 schemaHash 機制變得過于冗長。

4.2 encodeData 設計原理

encodeData 允許 Solidity 輕松實現 hashStruct 方法:

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

同時,它也可以在 EVM 內高效地進行原地計算:

function hashStruct(Mail memory mail) pure returns (bytes32 hash) {// 計算子哈希bytes32 typeHash = MAIL_TYPEHASH;bytes32 contentsHash = keccak256(mail.contents);assembly {// 備份內存let temp1 := mload(sub(mail, 32))let temp2 := mload(add(mail, 128))// 寫入 typeHash 和 contentsHashmstore(sub(mail, 32), typeHash)mstore(add(mail, 64), contentsHash)// 計算哈希hash := keccak256(sub(mail, 32), 128)// 恢復內存mstore(sub(mail, 32), temp1)mstore(add(mail, 64), temp2)}
}

原地計算的實現對結構體在內存中的布局做出了較強但合理的假設。具體而言,它假設結構體不會被分配到地址 32 以下的位置,成員按順序存儲,所有值都填充至 32 字節的邊界,并且動態類型和引用類型以 32 字節的指針形式存儲。

被否決的替代方案有:

  • 方案 6:緊湊打包(Tight Packing)
    在 Solidity 中,使用 keccak256 處理多個參數時,默認會采用緊湊打包的方式。這種方式可以最小化需要哈希的字節數,但在 EVM 中需要復雜的打包指令,因此不支持原地計算。
  • 方案 7:ABIv2 編碼
    隨著 abi.encode 的引入,可以使用 abi.encode 作為 encodeData 函數。然而,ABIv2 標準本身未能滿足確定性安全準則。相同數據可能存在多種有效的 ABIv2 編碼。此外,ABIv2 也不支持原地計算。
  • 方案 8:在 hashStruct 中省略 typeHash
    可以選擇不在 hashStruct 中包含 typeHash,而是將其與域分隔符(domain separator)合并。這種方式更高效,但會導致 Solidity keccak256 哈希函數的語義不具備單射性(injective)。
  • 方案 9:支持循環數據結構
    當前標準針對樹狀數據結構進行了優化,但未定義循環數據結構的處理方式。要支持循環數據結構,需要維護一個棧來記錄當前路徑,并在檢測到循環時使用棧偏移量進行替換。然而,這種方式的規范和實現都異常復雜,并且會破壞可組合性(composability),因為成員值的哈希值會依賴于遍歷路徑。
    同樣,直接實現該標準對于有向無環圖(DAG)來說也不是最優的。遞歸遍歷成員時,可能會多次訪問相同的節點。可以使用記憶化(memoization)來優化這一過程。

4.3 domainSeparator 的設計原理

由于不同的域(domain)有不同的需求,因此采用了一種可擴展的方案:DApp 指定一個 EIP712Domain 結構體類型,并創建一個 eip712Domain 實例,將其傳遞給用戶代理(user-agent)。用戶代理可以根據其中的字段采取不同的驗證措施。

5. 向后兼容性(Backwards Compatibility)

當前的 RPC 調用、web3 方法以及 SomeStruct.typeHash 參數尚未被定義。定義它們不應影響現有 DApp 的行為。

在 Solidity 中,表達式 keccak256(someInstance)(其中 someInstance 是結構體類型 SomeStruct 的一個實例)是有效的語法。當前,它計算的是該實例內存地址的 keccak256 哈希值。這種行為應被視為危險的,因為在某些情況下它可能表現正確,但在其他情況下可能會導致確定性失敗或單射性問題。依賴當前行為的 DApp 應被視為存在嚴重風險。

6. 測試用例

示例合約可以在 Example.sol 中找到,JavaScript 的簽名示例可以參考 Example.js。

// 示例合約
pragma solidity ^0.4.24;contract Example {struct EIP712Domain {string  name;string  version;uint256 chainId;address verifyingContract;}struct Person {string name;address wallet;}struct Mail {Person from;Person to;string contents;}bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");bytes32 constant PERSON_TYPEHASH = keccak256("Person(string name,address wallet)");bytes32 constant MAIL_TYPEHASH = keccak256("Mail(Person from,Person to,string contents)Person(string name,address wallet)");bytes32 DOMAIN_SEPARATOR;constructor () public {DOMAIN_SEPARATOR = hash(EIP712Domain({name: "Ether Mail",version: '1',chainId: 1,// verifyingContract: thisverifyingContract: 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC}));}function hash(EIP712Domain eip712Domain) internal pure returns (bytes32) {return keccak256(abi.encode(EIP712DOMAIN_TYPEHASH,keccak256(bytes(eip712Domain.name)),keccak256(bytes(eip712Domain.version)),eip712Domain.chainId,eip712Domain.verifyingContract));}function hash(Person person) internal pure returns (bytes32) {return keccak256(abi.encode(PERSON_TYPEHASH,keccak256(bytes(person.name)),person.wallet));}function hash(Mail mail) internal pure returns (bytes32) {return keccak256(abi.encode(MAIL_TYPEHASH,hash(mail.from),hash(mail.to),keccak256(bytes(mail.contents))));}function verify(Mail mail, uint8 v, bytes32 r, bytes32 s) internal view returns (bool) {// 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) == mail.from.wallet;}function test() public view returns (bool) {// Example signed messageMail memory mail = Mail({from: Person({name: "Cow",wallet: 0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826}),to: Person({name: "Bob",wallet: 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB}),contents: "Hello, Bob!"});uint8 v = 28;bytes32 r = 0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d;bytes32 s = 0x07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b91562;assert(DOMAIN_SEPARATOR == 0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f);assert(hash(mail) == 0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e);assert(verify(mail, v, r, s));return true;}
}

相應的JavaScript簽名示例代碼為:

// using ethereumjs-util 7.1.3
const ethUtil = require('ethereumjs-util');// using ethereumjs-abi 0.6.9
const abi = require('ethereumjs-abi');// using chai 4.3.4
const chai = require('chai');const typedData = {types: {EIP712Domain: [{ name: 'name', type: 'string' },{ name: 'version', type: 'string' },{ name: 'chainId', type: 'uint256' },{ name: 'verifyingContract', type: 'address' },],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: 1,verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',},message: {from: {name: 'Cow',wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',},to: {name: 'Bob',wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',},contents: 'Hello, Bob!',},
};const types = typedData.types;// Recursively finds all the dependencies of a type
function dependencies(primaryType, found = []) {if (found.includes(primaryType)) {return found;}if (types[primaryType] === undefined) {return found;}found.push(primaryType);for (let field of types[primaryType]) {for (let dep of dependencies(field.type, found)) {if (!found.includes(dep)) {found.push(dep);}}}return found;
}function encodeType(primaryType) {// Get dependencies primary first, then alphabeticallet deps = dependencies(primaryType);deps = deps.filter(t => t != primaryType);deps = [primaryType].concat(deps.sort());// Format as a string with fieldslet result = '';for (let type of deps) {result += `${type}(${types[type].map(({ name, type }) => `${type} ${name}`).join(',')})`;}return result;
}function typeHash(primaryType) {return ethUtil.keccakFromString(encodeType(primaryType), 256);
}function encodeData(primaryType, data) {let encTypes = [];let encValues = [];// Add typehashencTypes.push('bytes32');encValues.push(typeHash(primaryType));// Add field contentsfor (let field of types[primaryType]) {let value = data[field.name];if (field.type == 'string' || field.type == 'bytes') {encTypes.push('bytes32');value = ethUtil.keccakFromString(value, 256);encValues.push(value);} else if (types[field.type] !== undefined) {encTypes.push('bytes32');value = ethUtil.keccak256(encodeData(field.type, value));encValues.push(value);} else if (field.type.lastIndexOf(']') === field.type.length - 1) {throw 'TODO: Arrays currently unimplemented in encodeData';} else {encTypes.push(field.type);encValues.push(value);}}return abi.rawEncode(encTypes, encValues);
}function structHash(primaryType, data) {return ethUtil.keccak256(encodeData(primaryType, data));
}function signHash() {return ethUtil.keccak256(Buffer.concat([Buffer.from('1901', 'hex'),structHash('EIP712Domain', typedData.domain),structHash(typedData.primaryType, typedData.message),]),);
}const privateKey = ethUtil.keccakFromString('cow', 256);
const address = ethUtil.privateToAddress(privateKey);
const sig = ethUtil.ecsign(signHash(), privateKey);const expect = chai.expect;
expect(encodeType('Mail')).to.equal('Mail(Person from,Person to,string contents)Person(string name,address wallet)');
expect(ethUtil.bufferToHex(typeHash('Mail'))).to.equal('0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2',
);
expect(ethUtil.bufferToHex(encodeData(typedData.primaryType, typedData.message))).to.equal('0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8',
);
expect(ethUtil.bufferToHex(structHash(typedData.primaryType, typedData.message))).to.equal('0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e',
);
expect(ethUtil.bufferToHex(structHash('EIP712Domain', typedData.domain))).to.equal('0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f',
);
expect(ethUtil.bufferToHex(signHash())).to.equal('0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2');
expect(ethUtil.bufferToHex(address)).to.equal('0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826');
expect(sig.v).to.equal(28);
expect(ethUtil.bufferToHex(sig.r)).to.equal('0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d');
expect(ethUtil.bufferToHex(sig.s)).to.equal('0x07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b91562');

7. 安全性考量(Security Considerations)

7.1 重放攻擊(Replay Attacks)

本標準僅涉及消息簽名和簽名驗證。在許多實際應用中,簽名消息用于授權某項操作,如代幣交換。實施者必須確保應用程序在收到相同的簽名消息兩次時能夠正確處理。如,重復的消息應被拒絕,或者授權的操作應具有冪等性。具體實現方式取決于應用場景,超出了本標準的范圍。

7.2 交易搶跑攻擊(Frontrunning Attacks)

可靠地廣播簽名的機制取決于具體的應用,超出了本標準的范圍。當簽名被廣播到區塊鏈并用于合約時,應用程序必須能夠防范搶跑攻擊。在這種攻擊中,攻擊者攔截簽名并在原始預期用途發生之前將其提交到合約。應用程序應確保在攻擊者率先提交簽名時仍能正確處理,如拒絕該簽名,或僅產生與簽名者預期完全相同的效果。

參考資料

[1] EIP-712: Typed structured data hashing and signing – A procedure for hashing and signing of typed structured data as opposed to just bytestrings.

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/74551.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/74551.shtml
英文地址,請注明出處:http://en.pswp.cn/web/74551.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

contourformer:實時的輪廓分割transformer

論文地址:https://arxiv.org/abs/2501.17688 github:https://github.com/talebolano/Contourformer 模型結構 框架建立在 D-FINE 對象檢測模型之上,并將邊界框的回歸擴展到輪廓的回歸。為了實現高效的訓練,Contourformer 采用迭代方法進行輪廓變形,并引入降噪機制來加速…

【JavaScript】原型鏈 prototype 和 this 關鍵字的練習(老虎機)

這個老虎機練習主要考察JavaScript中的原型鏈(prototype)和this關鍵字的使用。 主要思路 創建三個輪盤(reels)實例:我們需要創建3個獨立的輪盤對象,它們都委托(delegate)到基礎的ree…

vue項目data functions should return an object

在vue項目中提示錯誤,data functions should return an object Message.error(err)錯了,Message.error()是element-ui的組件,只能接受字符串,不能接受對象。 改為Message.error(err.message)就好了 我的錯誤是 Message.error(er…

leetcode刷題 - 數組理論基礎

數組是內存空間連續存儲、相同類型數據的集合。遍歷方式:下標索引 下標:從 0 開始 數組的元素不能刪除,只能覆蓋 定義一維數組: int arr0[10]; int arr1[10] { 100, 90,80,70,60,50,40,30,20,10 }; int arr2[ ] { 100,90,80,7…

狀態機思想編程練習

狀態機實現LED流水燈 本次實驗,我們將利用狀態機的思想來進行Verilog編程實現一個LED流水燈,并通過Modelsim來進行模擬仿真,再到DE2-115開發板上進行驗證。 ? 首先進行主要代碼的編寫。 module led (input sys_clk,input sys_…

數據結構|排序算法(一)快速排序

一、排序概念 排序是數據結構中的一個重要概念,它是指將一組數據元素按照特定的順序進行排列的過程,默認是從小到大排序。 常見的八大排序算法: 插入排序、希爾排序、冒泡排序、快速排序、選擇排序、堆排序、歸并排序、基數排序 二、快速…

如何確保MQ消息隊列不丟失:Java實現與流程分析

前言 在分布式系統中,消息隊列(Message Queue, MQ)是核心組件之一,用于解耦系統、異步處理和削峰填谷。然而,消息的可靠性傳遞是使用MQ時需要重點考慮的問題。如果消息在傳輸過程中丟失,可能會導致數據不一…

關于termux運行pc交叉編譯的aarch64 elf的問題

在Linux系統上交叉編譯Nim程序到Android Termux環境需要特殊處理,以下是詳細的解決方案: 問題根源分析 ??ABI不兼容?? Android使用bionic libc而非標準glibc,直接編譯的Linux ARM二進制無法直接運行 ??動態鏈接錯誤?? 默認編譯會鏈…

為PXIe控制器配置NI Linux實時操作系統安裝軟件

一、升級BIOS 使用NI Linux Real-Time操作系統的PXI硬件支持頁面來確定NI Linux Real-Time是否支持您的PXIe控制器,以及是否需要更新控制器BIOS。 按照BIOS下載頁面上的“安裝說明”部分安裝BIOS更新。 注意:NI在NI 2020軟件版本中刪除對cRIO的Phar Lap和…

《汽車噪聲控制》課程作業

作業內容 在MATLAB繪制給出單個正弦波或余弦波的時域圖和頻域圖 繪制實測數據的時域圖和頻域圖 圖1 單個正弦波的時頻圖 圖1 單個正弦波的時頻圖 % 正弦波參數設置 f0 1000; % 信號頻率 1kHz Fs 16384; % 采樣頻率 16kHz T 0.05; % 信號持續時間 0.05秒 A 0.8; % 信號幅度…

Baklib內容中臺AI技術協同應用

內容中臺與AI協同創新 在數字化轉型進程中,內容中臺通過人工智能技術的深度整合,正重塑企業信息管理范式。以Baklib內容中臺為例,其通過智能語義分析引擎解析用戶意圖,結合知識圖譜構建技術動態關聯碎片化信息,實現從…

壓測工具開發實戰篇(二)——構建側邊欄以及設置圖標字體

你好,我是安然無虞。 文章目錄 構建側邊欄QtAwesome使用調整側邊欄寬度了解: sizePolicy屬性偽狀態 在閱讀本文之前, 有需要的老鐵可以先回顧一下上篇文章: 壓測工具開發(一)——使用Qt Designer構建簡單界面 構建側邊欄 我們要實現類似于下面這樣的側邊欄功能: …

Axure RP9.0教程: 查詢條件隱藏與顯示(綜合了動態面板狀態切換及展開收縮效果實現)

文章目錄 引言I 原型顯示/隱藏搜索框思路步驟詳細操作II 若依 ruoyi 顯示/隱藏搜索框 & 顯示隱藏列自定義設置顯示隱藏列顯示/隱藏搜索框引言 數據篩選有大量的查詢條件時,可以選擇查詢隱藏效果。 I 原型顯示/隱藏搜索框 綜合了動態面板狀態切換及展開收縮效果實現 思…

解鎖工業通信:Profibus DP到ModbusTCP網關指南!

解鎖工業通信:Profibus DP到ModbusTCP網關指南! 在工業自動化領域,隨著技術的不斷進步和應用場景的日益復雜,不同設備和系統之間的通訊協議兼容性問題成為了工程師們面臨的一大挑戰。尤其是在Profibus DP和Modbus/TCP這兩種廣泛應…

3維格式轉換(二)

基于python的三維模型演化可視化 本項目的主要內容為總結了3種不同的可視化方案( trimesh + matplotlib 庫、 pyvista 庫、 vedo 庫),并通過案例對可視化效果進行展示,最終通過模型動態演化案例給出最佳效果的可視化方案 本期結構圖為 本期博客結構圖 0 環境搭建 項目開…

docker導出image再導入到其它docker中

導出image docker save -o gxc_tenant.tar vue_tenant:1.0 eitc_tenant:1.0 redis:latest docker.io/mysql:8.0 minio/minio導入image docker load -i gxc_tenant.tar

Spring-IOC部分

Spring-IOC部分 1.SpringBean的配置詳解(Bean標簽) (1)scope 默認情況下,單純的Spring環境Bean的作用范圍有兩個:Singleton和Prototype singleton:單例,默認值,Spring…

人工智能爬蟲導致維基共享資源帶寬需求激增 50%

2025 年 4 月 1 日,維基媒體基金會在博文中表示,自 2024 年 1 月以來,維基共享資源下載多媒體的帶寬消耗激增 50%,這一變化趨勢主要由用于 AI 訓練數據集的網絡爬蟲導致。以下是具體分析1: 爬蟲流量特征與數據存儲模式…

2007-2019年各省地方財政交通運輸支出數據

2007-2019年各省地方財政交通運輸支出數據 1、時間:2007-2019年 2、來源:國家統計局、統計年鑒 3、指標:行政區劃代碼、地區、年份、地方財政交通運輸支出 4、范圍:31省 5、指標說明:地方財政交通運輸支出是指地方…

【爬蟲開發】爬蟲開發從0到1全知識教程第14篇:scrapy爬蟲框架,介紹【附代碼文檔】

本教程的知識點為:爬蟲概要 爬蟲基礎 爬蟲概述 知識點: 1. 爬蟲的概念 requests模塊 requests模塊 知識點: 1. requests模塊介紹 1.1 requests模塊的作用: 數據提取概要 數據提取概述 知識點 1. 響應內容的分類 知識點&#xff1a…