Files
oci-portal/internal/service/ethsig.go
T
2026-07-30 12:23:05 +08:00

90 lines
2.6 KiB
Go

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
}