从 go-ethereum 实施以太坊 personal_sign (EIP-191) 给出与 ethers.js 不同的签名

Implementing Ethereum personal_sign (EIP-191) from go-ethereum gives different signature from ethers.js

我正在尝试在 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

戈朗:



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

JavaScript 代码中存在错误。

signer.signMessage() 的文档(参见 Note 部分)看来,字符串是 UTF8 编码的,二进制数据必须作为 TypedArrayArray.
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

两个签名匹配 除了最后一个字节

JavaScript 代码 returns 格式为 r|s|v 的签名(参见 here)。 v 是一个字节大小,只是两个签名不同的值。

它是 v = 27 + rid,其中 rid 是恢复 ID。恢复 ID 的值介于 0 和 3 之间,因此 v 的值介于 27 和 30 或 0x1b 和 0x1e 之间(参见 here)。

Go 代码,另一方面,returns 最后一个字节中的恢复 ID 而不是 v。为了使 Go 代码的签名也与最后一个字节中的 JavaScript 代码的签名匹配,恢复 ID 必须替换为 v:

signatureBytes[64] += 27
fmt.Println("0x" + hex.EncodeToString(signatureBytes))