如何用比特幣私鑰創建以太坊地址

Chi-Hung Cheng
CryptoCow
Published in
10 min readDec 24, 2018

#crytocow

在區塊鏈的錢包中,私鑰可以產生出公鑰,而反過來要想從公鑰推算出私鑰則是不可能的。用公鑰加密的資訊可以用私鑰來解密,而用私鑰簽名的資訊則由公鑰來驗證,驗證通過後才能證明該資訊確實為私鑰持有人所發布。以 BTC 為例的話,在這個過程中最重要的角色的就是" 橢圓曲線加密算法"。

有些人會以為 BTC 跟 ETH 是不同的鏈所以用的橢圓曲線並不相同,但事實上兩個鏈使用的都是相同的 secp256k1 曲線,所以獲得公鑰的方式完全一樣,差別在從公鑰生成地址的過程,接下來我們會先介紹如何安全的生成私鑰,然後說明 ETH 如何從地址驗證由私鑰生成的公鑰。

Vitalik 曾經在 Ethereum Community Forum 上回覆過為何不使用其他曲線

以下整理自 Timur BadretdinovfreeCodeCamp 上的一系列文章,對原文及完整原始碼有興趣的可以去翻翻。

私鑰的規格

私鑰必須為正整數且必須小於 secp256k1 曲線的階 (secp256k1 的階為FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141),每個點可由一組 256 位元代表,而 256 位元正好是 32 個位元組,所以我們需要提供這個曲線演算法 32 個位元組的數據。

換句話說,BTC 及 ETH 的私鑰都是一組 32 位元組的字串,但它也可以是二進制字串、Base64字串、WIF 密鑰助記碼( mnemonic phrase )、十六進制字串。

相同的私鑰,以不同的格式編寫。

安全的私鑰生成

既然都知道他們使用的是同一條曲線,那我們其實就可以使用 BTC 社群比較信任的 bitaddress.org 來生成我們的私鑰,(用 MEW 或 Metamask 也都是不錯的選擇,至少他可以不是一串裸露在外的私鑰),但如果有良好安全意識的話,我們甚至不應該用瀏覽器來生成我們重要的私鑰 (可以看看 Reddit 上的討論),所以我們將用 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)

生成私鑰

首先使用我們的池生成 32 位元的數字,並確保我們的私鑰在範圍內(1, CURVE_ORDER),然後為了方便,我們轉為十六進制並刪除 0x 的部分。

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')

錢包地址

要從公鑰創建地址時,我們只需要將公鑰帶入 Keccak-256 (你可能會聽到一些人稱呼他為"卡咖 256"),然後獲得回傳值的最後 20 個 位元組。沒有 Base58 或任何其他轉換,唯一需要的是在地址的開頭添加 0x

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:]

校驗和 (ERC-55)

比特幣通過將公鑰雜湊後並獲得回傳值的前 4 個 位元組來創建校驗和,如果不添加校驗和則無法獲得有效地址。

但以太坊一開始並沒有校驗和機制來驗證公鑰的完整性。直到 Vitalik Buterin 在 2016 年時引入了校驗和機制,也就是 EIP-55,並且後來被各家錢包和交易所採用。

將校驗和添加到以太坊錢包地址使其區分大小寫

首先,獲得地址的 Keccak-256 雜湊值。需要注意的是,將此地址傳遞至雜湊函數時不能有0x的部分。

其次,依序迭代初始地址的 位元組。如果雜湊值的第 i 個 位元組大於或等於 8,則將第 i 個地址的字符轉換為大寫,否則將其保留為小寫。

最後,在回傳的字串開頭加回0x。如果忽略大小寫,校驗和地址會與初始地址相同。但使用大寫字母的地址讓任何人都能檢驗地址是否有效。

此校驗和有幾個好處:

  1. 向後兼容許多接受混合大小寫的十六進制解析器,將來也能輕鬆引入
  2. 保持長度為 40 個字元
  3. 平均每個地址將有 15 個校驗位,如果輸入錯誤,隨機生成的地址意外通過檢查的淨概率將為0.0247%,雖然不如 4 位元組的校驗代碼好,但比 ICAP 提高了約 50 倍
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)

結論

為以太坊創建錢包地址相較於比特幣簡單得多 (關於比特幣地址的部分為避免篇幅過長請直接參考作者的另一篇文章)。我們需要做的就只是將私鑰丟到 橢圓曲線,然後再把得到的公鑰丟到 Keccak-256,最後擷取該雜湊值的後面 20 個 位元組。

--

--