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") } }