Files
2026-07-30 12:23:05 +08:00

137 lines
4.2 KiB
Go

package service
import (
"encoding/hex"
"errors"
"strings"
"testing"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
)
// ethAddrOf 从私钥派生 EIP-55 地址(keccak256(未压缩公钥[1:]) 后 20 字节)。
func ethAddrOf(priv *secp256k1.PrivateKey) string {
return eip55(keccak256(priv.PubKey().SerializeUncompressed()[1:])[12:])
}
// testPrivKey 构造确定性的测试私钥;seed 填入 32 字节最低位。
func testPrivKey(seed byte) *secp256k1.PrivateKey {
raw := make([]byte, 32)
raw[31] = seed
return secp256k1.PrivKeyFromBytes(raw)
}
// signPersonal 用 dcrec 标准路径生成 personal_sign 签名(r||s||v 的 hex);
// legacyV 为 true 时 v 取 27/28,否则 0/1——两种取值链上钱包都会出现。
func signPersonal(priv *secp256k1.PrivateKey, message string, legacyV bool) string {
compact := ecdsa.SignCompact(priv, personalSignDigest([]byte(message)), false)
sig := make([]byte, 65)
copy(sig, compact[1:])
v := compact[0] // 27 或 28(未压缩公钥)
if !legacyV {
v -= 27
}
sig[64] = v
return "0x" + hex.EncodeToString(sig)
}
// TestEip55Vectors 用 EIP-55 规范文档中的官方校验和向量。
func TestEip55Vectors(t *testing.T) {
vectors := []string{
"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",
"0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB",
"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb",
}
for _, want := range vectors {
t.Run(want[:10], func(t *testing.T) {
for _, input := range []string{strings.ToLower(want), "0x" + strings.ToUpper(want[2:])} {
got, err := normalizeEthAddress(input)
if err != nil {
t.Fatalf("normalizeEthAddress(%s): %v", input, err)
}
if got != want {
t.Errorf("normalizeEthAddress(%s) = %s, want %s", input, got, want)
}
}
})
}
}
func TestNormalizeEthAddressInvalid(t *testing.T) {
tests := []struct {
name string
in string
}{
{name: "缺前缀", in: "5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed00"},
{name: "长度不足", in: "0x5aAeb6"},
{name: "非 hex 字符", in: "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAzz"},
{name: "空串", in: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if _, err := normalizeEthAddress(tt.in); !errors.Is(err, ErrWalletAddress) {
t.Errorf("normalizeEthAddress(%q) err = %v, want ErrWalletAddress", tt.in, err)
}
})
}
}
// TestKnownPrivKeyAddress 用公开锚点(私钥 0x…01 的以太坊地址)校验派生路径,
// 防止 keccak / 公钥序列化环节自洽但整体错误。
func TestKnownPrivKeyAddress(t *testing.T) {
got := ethAddrOf(testPrivKey(1))
want := "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"
if got != want {
t.Fatalf("address of privkey 0x01 = %s, want %s", got, want)
}
}
func TestRecoverEthAddressRoundTrip(t *testing.T) {
priv := testPrivKey(7)
addr := ethAddrOf(priv)
const msg = "demo.example.com wants you to sign in with your Ethereum account:\n0xabc"
tests := []struct {
name string
sig func() string
wantErr bool
}{
{name: "v=27/28", sig: func() string { return signPersonal(priv, msg, true) }},
{name: "v=0/1", sig: func() string { return signPersonal(priv, msg, false) }},
{name: "签名过短", sig: func() string { return "0x0102" }, wantErr: true},
{name: "v 非法", sig: func() string {
s := signPersonal(priv, msg, true)
return s[:len(s)-2] + "63" // v=99
}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
raw, err := hex.DecodeString(strings.TrimPrefix(tt.sig(), "0x"))
if err != nil {
t.Fatalf("decode sig: %v", err)
}
got, err := recoverEthAddress(personalSignDigest([]byte(msg)), raw)
if tt.wantErr {
if err == nil {
t.Fatal("recoverEthAddress: expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("recoverEthAddress: %v", err)
}
if got != addr {
t.Errorf("recovered = %s, want %s", got, addr)
}
})
}
// 篡改消息:恢复出的地址必然不同
raw, _ := hex.DecodeString(strings.TrimPrefix(signPersonal(priv, msg, true), "0x"))
got, err := recoverEthAddress(personalSignDigest([]byte(msg+"x")), raw)
if err == nil && got == addr {
t.Error("tampered message still recovered the signer address")
}
}