Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

如何用比特币私钥创建以太坊地址

人民干脆面
468 0 0
在区块链的钱包中,私钥可以产生出公钥,而反过来要想从公钥推算出私钥则是不可能的。用公钥加密的信息可以用私钥来解密,而用私钥签名的信息则由公钥来验证,验证通过后才能证明该信息确实为私钥持有人所发布。以 BTC 为例的话,在这个过程中最重要的角色的就是" 椭圆曲线加密算法"。
( p9 Z/ [1 w+ n! E7 I+ o+ E0 ~有些人会以为 BTC 跟 ETH 是不同的链所以用的椭圆曲线并不相同,但事实上两个链使用的都是相同的 secp256k1 曲线,所以获得公钥的方式完全一样,差别在从公钥生成地址的过程,接下来我们会先介绍如何安全的生成私钥,然后说明 ETH 如何从地址验证由私钥生成的公钥。
' i: B1 g' n2 T6 t3 ^  o; S& g/ l
; H7 c1 z% s1 U  k" U8 W私钥的规格3 o: \4 i* P- l2 b! a
私钥必须为正整数且必须小于 secp256k1 曲线的阶 (secp256k1 的阶为FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141),每个点可由一组 256位代表,而 256 位正好是 32 个字节,所以我们需要提供这个曲线算法 32 个字节的数据。
) \  n. O# n- t6 g- f6 [6 ]) w6 a换句话说,BTC 及 ETH的私钥都是一组 32 字节的字符串,但它也可以是二进制字符串、Base64字符串、WIF 密钥、助记码( mnemonic phrase )、十六进制字符串。9 V) f. Q( i" `5 x5 J
  
3 V3 {8 S7 x0 l; R3 p% N0 y相同的私钥,以不同的格式编写。
! Z. [; Y, e$ y' B4 R- `6 x: W* a  N  C! \4 G0 u
安全的私钥生成
7 c/ A: E; v' s6 ?% r6 H* M5 r既然都知道他们使用的是同一条曲线,那我们其实就可以使用 BTC 社群比较信任的 bitaddress.org 来生成我们的私钥,(用 MEW 或 Metamask 也都是不错的选择,至少他可以不是一串裸露在外的私钥),但如果有良好安全意识的话,我们甚至不应该用浏览器来生成我们重要的私钥 (可以看看 Reddit 上的讨论),所以我们将用 python 设计一个更简单的 bitaddress。
& a# f' n0 _& r4 {) l4 ?7 i3 q
) C# l6 h7 N  l) ]9 a1 P+ M了解 Bitaddress原理
- D2 R0 P7 L$ l1 F) \5 {Bitaddress 做了三件事情。首先,初始化字节数组,然后尝试从用户的计算机获得尽可能多的熵,根据用户的输入填满数组,最后生成私钥。
% i) m  `3 G. q: @% _4 k, RBitaddress 使用 256 字节的数组来存储熵。这个数组是被循环覆写的,所以当数组第一次填满时,索引变为零,然后覆写过程再次开始。. i" a5 k+ ?; Y& j* b% n; i
程序从 window.crypto 生成一个 256 字节的数组。然后写入一个时间戳来获得 4 个字节的熵。在这之后,它获得一些其他的数据包括屏幕大小,时区,浏览器扩充套件,地区等。来获得另外 6 个字节。
8 a0 P2 {. s' q: v( q初始化后,使用者持续输入来覆写初始字节。当移动光标时,程序会写入光标的位置。当按下按钮时,程序会写入按下的按钮的字符代码。
" r4 T5 Y2 b/ u+ W2 r  `最后,bitaddress 使用累积的熵来生成私钥。bitaddress 使用名为 ARC4 的 RNG算法。用当前时间以及收集的熵初始化ARC4,然后逐个取得字节,总共取 32 次。
5 L% ]* C: t* n7 I' N$ K% U初始化我们自己的种子池% K' x/ _) B6 b9 G! D  f1 V
我们从加密 RNG 和时间戳中写入一些字节。__seed_int 以及__seed_byte是将熵插入池的数组中的两个函式,而我们使用secrets生成我们的随机数。
2 ]5 y$ [: O- l+ @2 |7 ~2 Q! N6 z+ i8 ?( ]2 W& S7 X
def __init_pool(self):
! C5 y  d5 ^" d2 T- ~( ~0 T9 [8 F/ m( q    for i in range(self.POOL_SIZE):
0 r8 u! Q9 K( x3 W9 \$ C        random_byte = secrets.randbits(8)( f" l% s% u3 U8 L! b; B* F+ N, b
        self.__seed_byte(random_byte)* {( g" \3 z* ]: ?) s
    time_int = int(time.time())
' t4 u  f- N# |, G& Y' \) m8 [0 V2 |    self.__seed_int(time_int)
) C/ O2 i) H7 E% A# ndef __seed_int(self, n):2 \+ {) H8 c7 R$ k
    self.__seed_byte(n)
- f/ f- Y- Y' _$ m# q$ Y    self.__seed_byte(n >> 8)
! l6 s& O" V8 C6 Z1 d    self.__seed_byte(n >> 16)
7 j1 J4 q/ v: ]+ \7 M; E8 q0 d    self.__seed_byte(n >> 24)+ u4 J3 ?. [$ K7 o. Z2 t) R: i+ i3 r
def __seed_byte(self, n):
, v" f$ o5 ]4 M$ T  P7 r    self.pool[self.pool_pointer] ^= n & 255# c& i! K$ k$ L8 c8 e0 ?, g
    self.pool_pointer += 1
* h8 r% Z* n) m5 ^% K6 P; K4 h& h    if self.pool_pointer >= self.POOL_SIZE:
. `2 R/ x+ X0 w+ X8 w        self.pool_pointer = 0
+ C! x# s- k9 L1 b由输入填充种子池
) X. x2 e% W5 Q' U- J3 R/ A这里我们先写入一个时间戳,然后写入用户输入的字符串。+ K* j0 g& s# h; t0 @
* S, |: b9 T4 y, X
def seed_input(self, str_input):4 n8 V& i& Q/ Y/ ?1 g9 z
    time_int = int(time.time())
5 C1 K/ t  I/ `3 I0 I    self.__seed_int(time_int)
2 I6 ~6 a5 F, E$ X% b  |    for char in str_input:
. C, j* h' D, V! V! l        char_code = ord(char)
/ J* c& _( w) ^* a1 x        self.__seed_byte(char_code)
( X2 `9 Y9 r$ Q8 E* u/ }3 W生成私钥
, b6 s/ a# f. q! f5 S首先使用我们的池生成 32 位的数字,并确保我们的私钥在范围内(1, CURVE_ORDER),然后为了方便,我们转为十六进制并删除 0x 的部分。6 C" G# J3 v' O# a

  @; B5 v* J. U- ~def generate_key(self):
/ R) t, T- B( x2 ?9 D+ k    big_int = self.__generate_big_int()5 x; ^! I$ \8 ^( r3 \) q! g
    big_int = big_int % (self.CURVE_ORDER — 1) # key ; _$ j  k7 v8 C4 L5 K* \
    big_int = big_int + 1 # key > 0! }! K; c. B6 q7 M# m% y5 [" f$ g
    key = hex(big_int)[2:]; R) ?2 N: L/ _3 h
    return key
$ F: Z( o3 i" _3 Z) [# f* k6 C4 Qdef __generate_big_int(self):
8 A7 q) ~! {' n6 F* ^7 I; R4 E0 P    if self.prng_state is None:. ^; _( q8 v, G) y9 y8 G9 e
      seed = int.from_bytes(self.pool, byteorder='big', signed=False)
; s* e3 |1 T! r6 \# F1 w      random.seed(seed)
: `. c5 V. m0 V& ?      self.prng_state = random.getstate()  Y; `' l2 u2 U  v9 r* d: \
    random.setstate(self.prng_state)
+ X5 l) L/ ^1 d2 ~" i/ G    big_int = random.getrandbits(self.KEY_BYTES * 8)- @. l9 `) K- D- f, F
    self.prng_state = random.getstate()
5 S3 t+ B  I! W2 Z    return big_int
- Z7 S3 c5 F5 P% W) m最后仅需三行就可以生成我们的私钥。
" S- I3 H! H; f' U' R, o. [
: G1 p$ J5 T! Akg = KeyGenerator()
/ k! u3 O7 ?: H7 n% y* nkg.seed_input(‘Truly random string. I rolled a dice and got 4.’)
. I1 U. X( w$ [! y: C8 s- Dkg.generate_key()
" x; X  k( R7 E: }: x) s8 z生成ETH公钥# R: t( ?) }& K
将我们刚刚的私钥代入椭圆曲线,我们会得到一个 64 字节的整数,它是两个 32 字节的整数,代表椭圆曲线上连接在一起的 X 点和 Y 点。# `2 @5 h5 w  G, `6 G$ P1 i% X7 C

1 V$ c" M' v, d, xprivate_key_bytes = codecs.decode(private_key, 'hex')
* r$ ^6 ?% h$ t8 o# 獲得 ECDSA 公鑰
2 a9 |% S6 w+ X9 G2 O3 {8 {8 F8 Ikey = ecdsa.SigningKey.from_string(private_key_bytes, curve=ecdsa.SECP256k1).verifying_key
  O  [1 ~8 T7 u7 L* Dkey_bytes = key.to_string()) i3 o7 `9 j7 X' I( z: a
key_hex = codecs.encode(key_bytes, 'hex')' v. e, x" v' U6 v6 C
钱包地址6 G1 h" }3 _1 o3 }; s
要从公钥创建地址时,我们只需要将公钥带入 Keccak-256 (你可能会听到一些人称呼他为"卡咖256"),然后获得回传值的最后 20 个字节。没有 Base58 或任何其他转换,唯一需要的是在地址的开头添加 0x。; A- L5 v! m5 G/ |/ a

9 I7 ~) ?$ ?5 ~, S" T$ S# Vpublic_key_bytes = codecs.decode(public_key, 'hex')
: w5 t9 b/ ~' Ukeccak_hash = keccak.new(digest_bits=256)
  Z* z  G, f* f2 d9 `keccak_hash.update(public_key_bytes)
* P0 U" }( u4 K4 \$ q" P! t! e4 |) \keccak_digest = keccak_hash.hexdigest()
$ m  }3 e" V" u# Take the last 20 bytes
3 Z3 \- {4 j. x0 R0 o. O2 s; @wallet_len = 40, U; r; j5 c8 d$ h7 x
wallet = '0x' + keccak_digest[-wallet_len:]' E6 P9 |  Y8 X$ D
校验和 (ERC-55)
- H6 t! }% H8 l  F# c7 q% Z" |2 Z) M. `比特币通过将公钥哈希后并获得回传值的前 4 个字节来创建校验和,如果不添加校验和则无法获得有效地址。1 `/ [' O- k; E6 B/ u
但以太坊一开始并没有校验和机制来验证公钥的完整性。直到 Vitalik Buterin 在 2016 年时引入了校验和机制,也就是 EIP-55,并且后来被各家钱包和交易所采用。
8 t7 }  z$ s! ?8 K% y7 K将校验和添加到以太坊钱包地址使其区分大小写& b& {# C6 [4 J7 f/ B2 t9 N/ P
首先,获得地址的 Keccak-256 哈希值。需要注意的是,将此地址传递至哈希函数时不能有0x的部分。- U% G/ r* I' w. z3 g! V1 t3 ~3 |
其次,依序迭代初始地址的字节。如果哈希值的第 i 个字节大于或等于 8,则将第 i 个地址的字符转换为大写,否则将其保留为小写。
7 z' y# W4 C7 S* c4 D# `最后,在回传的字符串开头加回0x。如果忽略大小写,校验和地址会与初始地址相同。但使用大写字母的地址让任何人都能检验地址是否有效。4 [2 F$ u, h" [" \; F- ^  {# P
此校验和有几个好处:& E$ a+ f7 M2 ~' l2 q
1.     向后兼容许多接受混合大小写的十六进制解析器,将来也能轻松引入;
" S* R& T* E3 d2.   保持长度为 40 个字符;
7 z) d$ o! k' t6 J1 p3.   平均每个地址将有 15 个校验位,如果输入错误,随机生成的地址意外通过检查的净概率将为0.0247%,虽然不如 4 字节的校验代码好,但比 ICAP 提高了约 50 倍;
2 ]* L! s5 b* i# ?( E$ E$ w: C! r: X& C) u9 |
checksum = '0x'+ Q5 Z: }" A8 l; C1 S* ?
# Remove '0x' from the address
; f! j9 G' J1 q% R3 baddress = address[2:]
0 D8 Y  A% s( B# @. qaddress_byte_array = address.encode('utf-8')
3 Y' l1 K) l3 G; [/ q6 B) D$ E, _keccak_hash = keccak.new(digest_bits=256)
- F0 ?" Y# Z, T- v, Kkeccak_hash.update(address_byte_array)! Q% ?8 I( o# I' `' f
keccak_digest = keccak_hash.hexdigest()
  @  N9 n# [* D- p/ ~2 Bfor i in range(len(address)):: @0 z% e: l% c0 B: t( N
    address_char = address keccak_char = keccak_digest. p1 x& u4 C! }. ^3 m) \* E
    if int(keccak_char, 16) >= 8:1 q+ s+ e# G& }' {6 b
        checksum += address_char.upper()& j: g% n3 Y3 h0 ~# E. o8 [
    else:  f, Q$ J3 ~/ t% W+ N& ^
        checksum += str(address_char)
& I/ H7 W5 L* @3 G1 K5 [/ ]总结) F3 [1 R$ ?4 F0 e) k6 H% P
为以太坊创建钱包地址相较于比特币简单得多。我们需要做的就只是将私钥丢到椭圆曲线,然后再把得到的公钥丢到Keccak-256,最后撷取该哈希值的后面 20 个字节。
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

人民干脆面 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    9