有些人會以為BTC跟ETH是不同的鏈所以用的橢圓曲線並不相同,但事實上兩個鏈使用的都是相同的secp256k1曲線,所以獲得公鑰的方式完全一樣,差別在從公鑰生成地址的過程,接下來我們會先介紹如何安全的生成私鑰,然後說明ETH如何從地址驗證由私鑰生成的公鑰。
Vitalik曾經在Ethereum Community Forum上回復過為何不使用其他曲線
以下整理自 Timur Badretdinov 在 freeCodeCamp 上的一系列文章。私鑰的規格secp256k1曲線上點的個數約有2²⁵⁶個,每個點可由一組256位代表,而256位正好是32個位元組,所以我們需要提供這個曲線演算法32個位元組的資料。並且因為我們使用ECDSA,金鑰必須是正數且小於該曲線。換句話說,BTC及ETH的私鑰都是一組32位元組的字串,但它也可以是二進位制字串、Base64字串、WIF金鑰、助記碼(mnemonic phrase)、十六進位制字串。相同的私鑰,以不同的格式編寫。
安全的私鑰生成既然都知道他們使用的是同一條曲線,那我們其實就可以使用BTC社群比較信任的bitaddress.org來生成我們的私鑰,(用MEW或Metamask也都是不錯的選擇,至少他可以不是一串裸露在外的私鑰),但如果有良好安全意識的話,我們甚至不應該用瀏覽器來生成我們重要的私鑰,所以我們將用python設計一個更簡單的bitaddress。bitaddress.org提供了WIF格式的私鑰
瞭解Bitaddress原理Bitaddress做了三件事情。首先,初始化位元組陣列,然後嘗試從使用者的電腦獲得儘可能多的熵,根據使用者的輸入填滿陣列,最後生成私鑰。Bitaddress使用256位元組的陣列來儲存熵。這個陣列是被迴圈複寫的,所以當陣列第一次填滿時,索引變為零,然後複寫過程再次開始。程式從window.crypto生成一個256位元組的陣列。然後寫入一個時間戳來獲得4個位元組的熵。在這之後,它獲得一些其他的資料包括螢幕大小,時區,瀏覽器擴充套件,地區等。來獲得另外6個位元組。初始化後,使用者持續輸入來複寫初始位元組。當移動游標時,程式會寫入游標的位置。當按下按鈕時,程式會寫入按下的按鈕的字元程式碼。最後,bitaddress使用累積的熵來生成私鑰。bitaddress使用名為ARC4的RNG演算法。用當前時間以及收集的熵初始化ARC4,然後逐個取得位元組,總共取32次。初始化我們自己的種子池我們從加密RNG和時間戳中寫入一些位元組。__seed_int以及__seed_byte是將熵插入池的陣列中的兩個函式,而我們使用secrets生成我們的隨機數。def __init_pool(self): for i in range(self.POOL_SIZE): random_byte = secrets.randbits(8) self.__seed_byte(random_byte) time_int = int(time.time()) self.__seed_int(time_int) def __seed_int(self, n): self.__seed_byte(n) self.__seed_byte(n >> 8) self.__seed_byte(n >> 16) self.__seed_byte(n >> 24) def __seed_byte(self, n): self.pool[self.pool_pointer] ^= n & 255 self.pool_pointer += 1 if self.pool_pointer >= self.POOL_SIZE: self.pool_pointer = 0 |
由輸入填充種子池
這裡我們先寫入一個時間戳,然後寫入使用者輸入的字串。
def seed_input(self, str_input): time_int = int(time.time()) self.__seed_int(time_int) for char in str_input: char_code = ord(char) self.__seed_byte(char_code) |
def generate_key(self): big_int = self.__generate_big_int() big_int = big_int % (self.CURVE_ORDER — 1) # key < curve order big_int = big_int + 1 # key > 0 key = hex(big_int)[2:] return key def __generate_big_int(self): if self.prng_state is None: seed = int.from_bytes(self.pool, byteorder='big', signed=False) random.seed(seed) self.prng_state = random.getstate() random.setstate(self.prng_state) big_int = random.getrandbits(self.KEY_BYTES * 8) self.prng_state = random.getstate() return big_int |
kg = KeyGenerator() kg.seed_input(‘Truly random string. I rolled a dice and got 4.’) kg.generate_key() |
生成ETH公鑰
將我們剛剛的私鑰代入橢圓曲線,我們會得到一個64位元組的整數,它是兩個32位元組的整數,代表橢圓曲線上連線在一起的X點和Y點。
private_key_bytes = codecs.decode(private_key, 'hex') # 獲得 ECDSA 公鑰 key = ecdsa.SigningKey.from_string(private_key_bytes, curve=ecdsa.SECP256k1).verifying_key key_bytes = key.to_string() key_hex = codecs.encode(key_bytes, 'hex') |
public_key_bytes = codecs.decode(public_key, 'hex') keccak_hash = keccak.new(digest_bits=256) keccak_hash.update(public_key_bytes) keccak_digest = keccak_hash.hexdigest() # Take the last 20 bytes wallet_len = 40 wallet = '0x' + keccak_digest[-wallet_len:] |
checksum = '0x' # Remove '0x' from the address address = address[2:] address_byte_array = address.encode('utf-8') keccak_hash = keccak.new(digest_bits=256) keccak_hash.update(address_byte_array) keccak_digest = keccak_hash.hexdigest() for i in range(len(address)): address_char = address[i] keccak_char = keccak_digest[i] if int(keccak_char, 16) >= 8: checksum += address_char.upper() else: checksum += str(address_char) |