@@ -0,0 +1,89 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
// 以太坊 personal_sign(EIP-191)验签工具:仅覆盖 EOA 签名恢复,
|
||||
// 不做 EIP-1271 合约钱包(需链上 RPC,超出自托管零依赖边界)。
|
||||
|
||||
// errEthSig 是签名格式/恢复失败的内部标记;对外统一映射 ErrWalletSig。
|
||||
var errEthSig = errors.New("invalid ethereum signature")
|
||||
|
||||
// keccak256 计算 Keccak-256 摘要(以太坊场景的非 NIST 填充变体)。
|
||||
func keccak256(data ...[]byte) []byte {
|
||||
h := sha3.NewLegacyKeccak256()
|
||||
for _, d := range data {
|
||||
h.Write(d)
|
||||
}
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// personalSignDigest 计算 EIP-191 personal_sign 摘要:
|
||||
// keccak256("\x19Ethereum Signed Message:\n" + 消息字节长度十进制 + 消息)。
|
||||
func personalSignDigest(msg []byte) []byte {
|
||||
prefix := []byte("\x19Ethereum Signed Message:\n" + strconv.Itoa(len(msg)))
|
||||
return keccak256(prefix, msg)
|
||||
}
|
||||
|
||||
// recoverEthAddress 从 65 字节签名(r||s||v)恢复签名者地址(EIP-55 格式)。
|
||||
// v 兼容钱包生态的 27/28 与原始 0/1 两种取值;dcrec 的 compact 格式要求
|
||||
// 恢复位打头(27+v),与以太坊的尾置布局相反,需重排。
|
||||
func recoverEthAddress(digest, sig []byte) (string, error) {
|
||||
if len(sig) != 65 {
|
||||
return "", fmt.Errorf("%w: length %d", errEthSig, len(sig))
|
||||
}
|
||||
v := sig[64]
|
||||
if v >= 27 {
|
||||
v -= 27
|
||||
}
|
||||
if v > 1 {
|
||||
return "", fmt.Errorf("%w: recovery id %d", errEthSig, sig[64])
|
||||
}
|
||||
compact := make([]byte, 65)
|
||||
compact[0] = 27 + v
|
||||
copy(compact[1:], sig[:64])
|
||||
pub, _, err := ecdsa.RecoverCompact(compact, digest)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %v", errEthSig, err)
|
||||
}
|
||||
// 地址 = keccak256(未压缩公钥去掉 0x04 前缀)的后 20 字节
|
||||
return eip55(keccak256(pub.SerializeUncompressed()[1:])[12:]), nil
|
||||
}
|
||||
|
||||
// eip55 把 20 字节地址编码为 EIP-55 混合大小写校验和格式:
|
||||
// 对小写 hex 串取 keccak256,对应半字节 ≥ 8 的字母转大写。
|
||||
func eip55(addr []byte) string {
|
||||
lower := hex.EncodeToString(addr)
|
||||
sum := keccak256([]byte(lower))
|
||||
out := []byte(lower)
|
||||
for i, ch := range out {
|
||||
nibble := sum[i/2] >> 4
|
||||
if i%2 == 1 {
|
||||
nibble = sum[i/2] & 0x0f
|
||||
}
|
||||
if ch >= 'a' && nibble >= 8 {
|
||||
out[i] = ch - 'a' + 'A'
|
||||
}
|
||||
}
|
||||
return "0x" + string(out)
|
||||
}
|
||||
|
||||
// normalizeEthAddress 校验 0x+40hex 地址并返回 EIP-55 规范形;大小写不敏感。
|
||||
func normalizeEthAddress(addr string) (string, error) {
|
||||
if len(addr) != 42 || !strings.HasPrefix(addr, "0x") {
|
||||
return "", ErrWalletAddress
|
||||
}
|
||||
raw, err := hex.DecodeString(strings.ToLower(addr[2:]))
|
||||
if err != nil {
|
||||
return "", ErrWalletAddress
|
||||
}
|
||||
return eip55(raw), nil
|
||||
}
|
||||
Reference in New Issue
Block a user