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
相关产品推荐
相关产品推荐

