从 go-ethereum 实现 Ethereum personal_sign (EIP-191) 给出了与 ethers.js 不同的签名
Posted
技术标签:
【中文标题】从 go-ethereum 实现 Ethereum personal_sign (EIP-191) 给出了与 ethers.js 不同的签名【英文标题】:Implementing Ethereum personal_sign (EIP-191) from go-ethereum gives different signature from ethers.js 【发布时间】:2021-12-14 03:05:59 【问题描述】:我正在尝试在 Golang 中生成 personal_sign
,就像它在 ethers.js 中实现的一样。 Similar question 但最终还是使用了普通的 sign
而不是个人的 sign_implementation
。
以太币
// keccak256 hash of the data
let dataHash = ethers.utils.keccak256(
ethers.utils.toUtf8Bytes(JSON.stringify(dataToSign))
);
//0x8d218fc37d2fd952b2d115046b786b787e44d105cccf156882a2e74ad993ee13
let signature = await wallet.signMessage(dataHash); // 0x469b07327fc41a2d85b7e69bcf4a9184098835c47cc7575375e3a306c3718ae35702af84f3a62aafeb8aab6a455d761274263d79e7fc99fbedfeaf759d8dc9361c
Golang:
func signHash(data []byte) common.Hash
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)
return crypto.Keccak256Hash([]byte(msg))
privateKey, err := crypto.HexToECDSA(hexPrivateKey)
if err != nil
log.Fatal(err)
dataHash := crypto.Keccak256Hash(dataToSign) //0x8d218fc37d2fd952b2d115046b786b787e44d105cccf156882a2e74ad993ee13
signHash := signHash(dataHash.Bytes())
signatureBytes, err := crypto.Sign(signHash.Bytes(), privateKey)
if err != nil
log.Fatal(err)
// signatureBytes 0xec56178d3dca77c3cee7aed83cdca2ffa2bec8ef1685ce5103cfa72c27beb61313d91b9ad9b9a644b0edf6352cb69f2f8acd25297e3c64cd060646242e0455ea00
如你所见,哈希是相同的,但签名不同:
0x469b07327fc41a2d85b7e69bcf4a9184098835c47cc7575375e3a306c3718ae35702af84f3a62aafeb8aab6a455d761274263d79e7fc99fbedfeaf759d8dc9361c
以太币
0xec56178d3dca77c3cee7aed83cdca2ffa2bec8ef1685ce5103cfa72c27beb61313d91b9ad9b9a644b0edf6352cb69f2f8acd25297e3c64cd060646242e0455ea00
Golang
查看 Ethers.js 的源代码,除了填充的管理方式之外,我找不到任何不同之处。
编辑 检查批准的答案
signHash(data []byte) common.Hash
hexData := hexutil.Encode(data)
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(hexData), hexData)
return crypto.Keccak256Hash([]byte(msg))
【问题讨论】:
【参考方案1】:javascript 代码中存在错误。
从signer.signMessage()
的文档(请参阅注意 部分)看来,字符串是 UTF8 编码的,二进制数据必须作为 TypedArray
或 Array
传递。
Keccak 哈希返回十六进制编码,即作为字符串,因此是 UTF8 编码,这是不正确的。相反,它必须转换为TypedArray
。为此,库提供了函数ethers.utils.arrayify()
。
以下 JavaScript 基于发布的代码,但执行所需的十六进制解码:
(async () =>
let privateKey = "0x8da4ef21b864d2cc526dbdb2a120bd2874c36c9d0a1fb7f8c63d7f7a8b41de8f";
let dataToSign = "data1":"value1","data2":"value2";
let dataHash = ethers.utils.keccak256(
ethers.utils.toUtf8Bytes(JSON.stringify(dataToSign))
);
dataHashBin = ethers.utils.arrayify(dataHash)
let wallet = new ethers.Wallet(privateKey);
let signature = await wallet.signMessage(dataHashBin);
document.getElementById("signature").innerhtml = signature; // 0xfcc3e9431c139b5f943591af78c280b939595ce9df66210b7b8bb69565bdd2af7081a8acc0cbb5ea55bd0d673b176797966a5180c11ac297b7e6344c5822e66d1c
)();
<script src="https://cdn.ethers.io/lib/ethers-5.0.umd.min.js" type="text/javascript"></script>
<p style="font-family:'Courier New', monospace;" id="signature"></p>
产生以下签名:
0xfcc3e9431c139b5f943591af78c280b939595ce9df66210b7b8bb69565bdd2af7081a8acc0cbb5ea55bd0d673b176797966a5180c11ac297b7e6344c5822e66d1c
以下 Go 代码基于未修改的已发布 Go 代码,但使用 JavaScript 代码中的键和数据进行比较:
package main
import (
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"encoding/hex"
"encoding/json"
"log"
)
func signHash(data []byte) common.Hash
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)
return crypto.Keccak256Hash([]byte(msg))
func main()
hexPrivateKey := "8da4ef21b864d2cc526dbdb2a120bd2874c36c9d0a1fb7f8c63d7f7a8b41de8f"
dataMap := map[string]string"data1":"value1","data2":"value2"
dataToSign, _ := json.Marshal(dataMap)
privateKey, err := crypto.HexToECDSA(hexPrivateKey)
if err != nil
log.Fatal(err)
dataHash := crypto.Keccak256Hash(dataToSign) //0x8d218fc37d2fd952b2d115046b786b787e44d105cccf156882a2e74ad993ee13
signHash := signHash(dataHash.Bytes())
signatureBytes, err := crypto.Sign(signHash.Bytes(), privateKey)
if err != nil
log.Fatal(err)
fmt.Println("0x" + hex.EncodeToString(signatureBytes))
Go 代码给出以下签名:
0xfcc3e9431c139b5f943591af78c280b939595ce9df66210b7b8bb69565bdd2af7081a8acc0cbb5ea55bd0d673b176797966a5180c11ac297b7e6344c5822e66d01
两个签名都匹配最后一个字节的 except。
JavaScript 代码以r|s|v
格式返回签名(请参阅here)。 v
是一个字节大小,只是两个签名不同的值。
它是v = 27 + rid
,其中rid
是恢复ID。恢复 ID 的值介于 0 和 3 之间,因此 v
的值介于 27 和 30 或 0x1b 和 0x1e 之间(请参阅 here)。
另一方面,Go 代码在最后一个字节中返回恢复 ID,而不是 v
。为了使 Go 代码的签名也与最后一个字节的 JavaScript 代码的签名匹配,recovery ID 必须替换为v
:
signatureBytes[64] += 27
fmt.Println("0x" + hex.EncodeToString(signatureBytes))
【讨论】:
谢谢,这是正确的答案。因为要匹配 JS 实现,所以我修改了 go signHash 函数,将 Byte Array 编码为 Hex,固定最后一个字节后返回相同的签名。将在我的原始答案上发布代码作为更新以供将来参考。以上是关于从 go-ethereum 实现 Ethereum personal_sign (EIP-191) 给出了与 ethers.js 不同的签名的主要内容,如果未能解决你的问题,请参考以下文章
使用 Browser-solidity 在 Go-Ethereum 上进行简单的智能合约部署
基于Ubuntu系统搭建以太坊go-ethereum源码的开发环境