You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Geth ecrecover无效签名恢复ID:Golang库签名验证失败排查

嘿,这问题我熟!你肯定是没处理Ethereum签名验证里的EIP-191前缀——Etherscan的验证工具会自动给消息加这个前缀,但Geth库不会帮你做这件事,直接用原始消息验证自然会失败。

核心原因:EIP-191签名规范

Ethereum的消息签名为了防止被恶意当成交易执行,遵循EIP-191标准:签名前必须给原始消息拼接前缀 \x19Ethereum Signed Message:\n + 消息的字节长度字符串。钱包(比如你用来签CryptoKitties相关消息的钱包)会自动处理这个前缀,但Geth的底层验证函数(比如crypto.Ecrecover)是“裸”的,需要你手动处理前缀。

你之前的代码大概率是这样的(错误示例)
// 错误:直接用原始消息哈希验证
msg := []byte("你的测试消息")
hash := crypto.Keccak256Hash(msg)
recoveredAddr, _ := crypto.Ecrecover(hash.Bytes(), signature)

这种写法跳过了前缀拼接,验证的哈希和签名时的哈希完全不一样,肯定通不过。

修正后的两种实现方式

方式1:用Geth的sigutil包(推荐,封装更完善)

这个包已经帮你处理了EIP-191的前缀逻辑,不用手动拼接:

import (
    "fmt"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/signer/core/apitypes"
    "github.com/ethereum/go-ethereum/signer/sigutil"
)

func VerifyEthereumSignature(message string, signature []byte, expectedAddr common.Address) bool {
    // 构造符合EIP-191的TypedData结构
    typedData := apitypes.TypedData{
        Domain: apitypes.TypedDataDomain{
            Name:    "Ethereum Signed Message",
            Version: "1",
        },
        Types: apitypes.Types{
            "Message": []apitypes.Type{
                {Name: "content", Type: "string"},
            },
        },
        PrimaryType: "Message",
        Message: map[string]interface{}{
            "content": message,
        },
    }

    // 计算带前缀的消息哈希
    hash, err := sigutil.TypedDataHash(typedData)
    if err != nil {
        fmt.Printf("哈希计算失败:%v\n", err)
        return false
    }

    // 从签名中恢复地址
    recoveredPubKey, err := crypto.Ecrecover(hash, signature)
    if err != nil {
        fmt.Printf("地址恢复失败:%v\n", err)
        return false
    }

    // 把公钥转成Ethereum地址格式
    recoveredAddr := common.BytesToAddress(crypto.Keccak256(recoveredPubKey[1:])[12:])
    
    // 对比预期地址
    return recoveredAddr == expectedAddr
}

方式2:手动拼接前缀(更直观,适合理解原理)

如果你不想引入额外的包,直接手动拼接前缀也可以:

import (
    "fmt"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/crypto"
)

func VerifySimpleSignature(message string, signature []byte, expectedAddr common.Address) bool {
    // 拼接EIP-191标准前缀:"\x19Ethereum Signed Message:\n" + 消息长度的字符串
    prefix := fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(message))
    fullMessage := []byte(prefix + message)
    
    // 计算带前缀的消息哈希
    hash := crypto.Keccak256Hash(fullMessage)

    // 恢复地址
    recoveredPubKey, err := crypto.Ecrecover(hash.Bytes(), signature)
    if err != nil {
        fmt.Printf("地址恢复失败:%v\n", err)
        return false
    }

    recoveredAddr := common.BytesToAddress(crypto.Keccak256(recoveredPubKey[1:])[12:])
    return recoveredAddr == expectedAddr
}
额外要注意的细节
  • 签名的v值:有些钱包返回的签名v是0或1,这时候需要加27转成标准的27/28格式,否则Ecrecover会失败
  • 编码一致性:确保验证时的消息编码和签名时完全一致(比如都是UTF-8,不要有转义或编码差异)

试试上面的代码,应该就能和Etherscan的验证结果匹配了!

内容的提问来源于stack exchange,提问作者Levon Tamrazov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 07:08:12