首頁 > 軟體

python區塊鏈地址的簡版實現

2022-05-25 18:01:23

說明

本文根據https://github.com/liuchengxu/blockchain-tutorial 的內容,用python實現的,但根據個人的理解進行了一些修改,大量參照了原文的內容。文章末尾有"本節完整原始碼實現地址"。

引言

在上一篇文章中,我們已經初步實現了交易。相信你應該瞭解了交易中的一些天然屬性,這些屬性沒有絲毫“個人”色彩的存在:在位元幣中,沒有使用者賬戶,不需要也不會在任何地方儲存個人資料(比如姓名,護照號碼或者 SSN)。但是,我們總要有某種途徑識別出你是交易輸出的所有者(也就是說,你擁有在這些輸出上鎖定的幣)。這就是位元幣地址(address)需要完成的使命。在上一篇中,我們把一個由使用者定義的任意字串當成是地址,現在我們將要實現一個跟位元幣一樣的真實地址。

位元幣地址

這就是一個真實的位元幣地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa。這是史上第一個位元幣地址,據說屬於中本聰。位元幣地址是完全公開的,如果你想要給某個人傳送幣,只需要知道他的地址就可以了。但是,地址(儘管地址也是獨一無二的)並不是用來證明你是一個“錢包”所有者的信物。實際上,所謂的地址,只不過是將公鑰表示成人類可讀的形式而已,因為原生的公鑰人類很難閱讀。在位元幣中,你的身份(identity)就是一對(或者多對)儲存在你的電腦(或者你能夠獲取到的地方)上的公鑰(public key)和私鑰(private key)。位元幣基於一些加密演演算法的組合來建立這些金鑰,並且保證了在這個世界上沒有其他人能夠取走你的幣,除非拿到你的金鑰。下面,讓我們來討論一下這些演演算法到底是什麼。

公鑰加密

公鑰加密(public-key cryptography)演演算法使用的是成對的金鑰:公鑰和私鑰。公鑰並不是敏感資訊,可以告訴其他人。但是,私鑰絕對不能告訴其他人:只有所有者(owner)才能知道私鑰,能夠識別,鑑定和證明所有者身份的就是私鑰。在加密貨幣的世界中,你的私鑰代表的就是你,私鑰就是一切。

本質上,位元幣錢包也只不過是這樣的金鑰對而已。當你安裝一個錢包應用,或是使用一個位元幣使用者端來生成一個新地址時,它就會為你生成一對金鑰。在位元幣中,誰擁有了私鑰,誰就可以控制所有傳送到這個公鑰的幣。

私鑰和公鑰只不過是隨機的位元組序列,因此它們無法在螢幕上列印,人類也無法通過肉眼去讀取。這就是為什麼位元幣使用了一個轉換演演算法,將公鑰轉化為一個人類可讀的字串(也就是我們看到的地址)。

如果你用過位元幣錢包應用,很可能它會為你生成一個助記符。這樣的助記符可以用來替代私鑰,並且可以被用於生成私鑰。BIP-039 已經實現了這個機制。

好了,現在我們已經知道了在位元幣中證明使用者身份的是私鑰。那麼,位元幣如何檢查交易輸出(和儲存在裡面的幣)的所有權呢?

數位簽章

在數學和密碼學中,有一個數位簽章(digital signature)的概念,演演算法可以保證:

  • 當資料從傳送方傳送到接收方時,資料不會被修改;
  • 資料由某一確定的傳送方建立;
  • 傳送方無法否認傳送過資料這一事實。

通過在資料上應用簽名演演算法(也就是對資料進行簽名),你就可以得到一個簽名,這個簽名晚些時候會被驗證。生成數位簽章需要一個私鑰,而驗證簽名需要一個公鑰。簽名有點類似於印章,比方說我做了一幅畫,完了用印章一蓋,就說明了這幅畫是我的作品。給資料生成簽名,就是給資料蓋了章。

為了對資料進行簽名,我們需要下面兩樣東西:

  • 要簽名的資料
  • 私鑰

應用簽名演演算法可以生成一個簽名,並且這個簽名會被儲存在交易輸入中。為了對一個簽名進行驗證,我們需要以下三樣東西:

  • 被簽名的資料
  • 簽名
  • 公鑰

簡單來說,驗證過程可以被描述為:檢查簽名是由被簽名資料加上私鑰得來,並且公鑰恰好是由該私鑰生成。

資料簽名並不是加密,你無法從一個簽名重新構造出資料。這有點像雜湊:你在資料上執行一個雜湊演演算法,然後得到一個該資料的唯一表示。簽名與雜湊的區別在於金鑰對:有了金鑰對,才有簽名驗證。但是金鑰對也可以被用於加密資料:私鑰用於加密,公鑰用於解密資料。不過位元幣並不使用加密演演算法。

在位元幣中,每一筆交易輸入都會由建立交易的人簽名。在被放入到一個塊之前,必須要對每一筆交易進行驗證。除了一些其他步驟,驗證意味著:

  • 檢查交易輸入有權使用來自之前交易的輸出
  • 檢查交易簽名是正確的

如圖,對資料進行簽名和對簽名進行驗證的過程大致如下:

現在來回顧一個交易完整的生命週期:

  • 起初,創世塊裡面包含了一個 coinbase 交易。在 coinbase 交易中,沒有輸入,所以也就不需要簽名。coinbase 交易的輸出包含了一個雜湊過的公鑰(使用的是
  • RIPEMD16(SHA256(PubKey)) 演演算法)
  • 當一個人傳送幣時,就會建立一筆交易。這筆交易的輸入會參照之前交易的輸出。每個輸入會儲存一個公鑰(沒有被雜湊)和整個交易的一個簽名。
  • 位元幣網路中接收到交易的其他節點會對該交易進行驗證。除了一些其他事情,他們還會檢查:在一個輸入中,公鑰雜湊與所參照的輸出雜湊相匹配(這保證了傳送方只能花費屬於自己的幣);簽名是正確的(這保證了交易是由幣的實際擁有者所建立)。
  • 當一個礦工準備挖一個新塊時,他會將交易放到塊中,然後開始挖礦。
  • 當新塊被挖出來以後,網路中的所有其他節點會接收到一條訊息,告訴其他人這個塊已經被挖出並被加入到區塊鏈。
  • 當一個塊被加入到區塊鏈以後,交易就算完成,它的輸出就可以在新的交易中被參照。

橢圓曲線加密

正如之前提到的,公鑰和私鑰是隨機的位元組序列。私鑰能夠用於證明持幣人的身份,需要有一個條件:隨機演演算法必須生成真正隨機的位元組。因為沒有人會想要生成一個私鑰,而這個私鑰意外地也被別人所有。

位元幣使用橢圓曲線來產生私鑰。橢圓曲線是一個複雜的數學概念,我們並不打算在這裡作太多解釋(如果你真的十分好奇,可以檢視這篇文章,注意:有很多數學公式!)我們只要知道這些曲線可以生成非常大的亂數就夠了。在位元幣中使用的曲線可以隨機選取在 0 與 2 ^ 2 ^ 56(大概是 10^77, 而整個可見的宇宙中,原子數在 10^78 到 10^82 之間) 的一個數。有如此高的一個上限,意味著幾乎不可能發生有兩次生成同一個私鑰的事情。

位元幣使用的是 ECDSA(Elliptic Curve Digital Signature Algorithm)演演算法來對交易進行簽名,我們也會使用該演演算法。

Base58

回到上面提到的位元幣地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa 。現在,我們已經知道了這是公鑰用人類可讀的形式表示而已。如果我們對它進行解碼,就會看到公鑰的本來面目(16 進位製表示的位元組):

0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93

位元幣使用 Base58 演演算法將公鑰轉換成人類可讀的形式。這個演演算法跟著名的 Base64 很類似,區別在於它使用了更短的字母表:為了避免一些利用字母相似性的攻擊,從字母表中移除了一些字母。也就是,沒有這些符號:0(零),O(大寫的 o),I(大寫的i),l(小寫的 L),因為這幾個字母看著很像。另外,也沒有 + 和 / 符號。

下圖是從一個公鑰獲得一個地址的過程:

因此,上面提到的公鑰解碼後包含三個部分:

Version  Public key hash                           Checksum
00       62E907B15CBF27D5425399EBF6F0FB50EBB88F18  C29B7D93

由於雜湊函數是單向的(也就說無法逆轉回去),所以不可能從一個雜湊中提取公鑰。不過通過執行雜湊函數並進行雜湊比較,我們可以檢查一個公鑰是否被用於雜湊的生成。

好了,所有細節都已就緒,來寫程式碼吧。很多概念只有當寫程式碼的時候,才能理解地更透徹。

實現地址

我們先從錢包 Wallet 結構開始:

class Wallet(object):
    # hex version
    VERSION = b''
    def __init__(self, private_key):
        self._private_key = private_key
        self._public_key = private_key.get_verifying_key()
        self._address = ''
    @classmethod
    def generate_wallet(cls, curve=SECP256k1):
        """
        generate a wallet
        """
        sign_key = SigningKey.generate(curve=curve)
        return cls(sign_key)

一個錢包只有一個金鑰對而已。我們需要 Wallets 型別來儲存多個錢包的組合,將它們儲存到檔案中,或者從檔案中進行載入。Wallet 的建構函式會生成一個新的金鑰對。generate_wallet 函數非常直觀:ECDSA 基於橢圓曲線,所以我們需要一個橢圓曲線。接下來,使用橢圓生成一個私鑰。

@property
def address(self):
    if not self._address:
        prv_addr = self.VERSION + self._hash_public_key()
        self._address = base58.b58encode_check(prv_addr)
    return self._address

以上是地址的生成,使用base58的庫來實現,通過pip install base58即可安裝,我們也可以直接下載原始碼放在工程目錄下使用。使用該庫我們就不用重複造輪子了。

至此,就可以得到一個真實的位元幣地址,你甚至可以在 blockchain.info 檢視它的餘額。不過我可以負責任地說,無論生成一個新的地址多少次,檢查它的餘額都是 0。這就是為什麼選擇一個合適的公鑰加密演演算法是如此重要:考慮到私鑰是亂數,生成同一個數位的概率必須是儘可能地低。理想情況下,必須是低到“永遠”不會重複。

另外,注意:你並不需要連線到一個位元幣節點來獲得一個地址。地址生成演演算法使用的多種開源演演算法可以通過很多程式語言和庫實現。

注意,現在我們已經不再需要 script_pub_key 和 script_sig 欄位,因為我們不會實現一個指令碼語言。相反,script_sig 會被分為 signature 和 pub_key 欄位,script_pub_key 被重新命名為 PubKeyHash。我們會實現跟位元幣裡一樣的輸出鎖定/解鎖和輸入簽名邏輯,不同的是我們會通過方法(method)來實現。

use_key 方法檢查輸入使用了指定金鑰來解鎖一個輸出。注意到輸入儲存的是原生的公鑰(也就是沒有被雜湊的公鑰),但是這個函數要求的是雜湊後的公鑰。is_locked_with_key 檢查是否提供的公鑰雜湊被用於鎖定輸出。這是一個 use_key 的輔助函數,並且它們都被用於 find_unspent_transactions 來形成交易之間的聯絡。

lock 只是簡單地鎖定了一個輸出。當我們給某個人傳送幣時,我們只知道他的地址,因為這個函數使用一個地址作為唯一的引數。然後,地址會被解碼,從中提取出公鑰雜湊並儲存在 pub_key_hash 欄位。

實現簽名

交易必須被簽名,因為這是位元幣裡面保證傳送方不會花費屬於其他人的幣的唯一方式。如果一個簽名是無效的,那麼這筆交易就會被認為是無效的,因此,這筆交易也就無法被加到區塊鏈中。

我們現在離實現交易簽名還差一件事情:用於簽名的資料。一筆交易的哪些部分需要簽名?又或者說,要對完整的交易進行簽名?選擇簽名的資料相當重要。因為用於簽名的這個資料,必須要包含能夠唯一識別資料的資訊。比如,如果僅僅對輸出值進行簽名並沒有什麼意義,因為簽名不會考慮傳送方和接收方。

考慮到交易解鎖的是之前的輸出,然後重新分配裡面的價值,並鎖定新的輸出,那麼必須要簽名以下資料:

  • 儲存在已解鎖輸出的公鑰雜湊。它識別了一筆交易的“傳送方”。
  • 儲存在新的鎖定輸出裡面的公鑰雜湊。它識別了一筆交易的“接收方”。
  • 新的輸出值。

在位元幣中,鎖定/解鎖邏輯被儲存在指令碼中,它們被分別儲存在輸入和輸出的 script_sig 和 script_pub_key 欄位。由於位元幣允許這樣不同型別的指令碼,它對 script_pub_key 的整個內容進行了簽名。

可以看到,我們不需要對儲存在輸入裡面的公鑰簽名。因此,在位元幣裡, 所簽名的並不是一個交易,而是一個去除部分內容的輸入副本,輸入裡面儲存了被參照輸出的 script_pub_key 。

獲取修剪後的交易副本的詳細過程在這裡. 雖然它可能已經過時了,但是我並沒有找到另一個更可靠的來源。

看著有點複雜,來開始寫程式碼吧。先從 sign 方法開始:

def sign(self, priv_key, prev_txs):
        if self.is_coinbase():
            return
        tx_copy = self._trimmed_copy()
        for in_id, vin in enumerate(tx_copy.vins):
            prev_tx = prev_txs.get(vin.txid, None)
            if not prev_tx:
                raise ValueError('Previous transaction is error')
            tx_copy.vins[in_id].signature = None
            tx_copy.vins[in_id].pub_key = prev_tx.vouts[vin.vout].pub_key_hash
            tx_copy.set_id()
            tx_copy.vins[in_id].pub_key = None
            sk = ecdsa.SigningKey.from_string(
                binascii.a2b_hex(priv_key), curve=ecdsa.SECP256k1)
            sign = sk.sign(tx_copy.txid.encode())
            self.vins[in_id].signature = binascii.hexlify(sign).decode()

coinbase沒有輸入所以不需要簽名。

tx_copy = self._trimmed_copy()

將會被簽署的是修剪後的交易副本,而不是一個完整交易:

    def _trimmed_copy(self):
        inputs = []
        outputs = []
        for vin in self.vins:
            inputs.append(TXInput(vin.txid, vin.vout, None))
        for vout in self.vouts:
            outputs.append(TXOutput(vout.value, vout.pub_key_hash))
        tx = Transaction(inputs, outputs)
        tx.txid = self.txid
        return tx

接下來,我們會迭代副本中每一個輸入:

for in_id, vin in enumerate(tx_copy.vins):
    prev_tx = prev_txs.get(vin.txid, None)
    if not prev_tx:
        raise ValueError('Previous transaction is error')
    tx_copy.vins[in_id].signature = None
    tx_copy.vins[in_id].pub_key = prev_tx.vouts[vin.vout].pub_key_hash

在每個輸入中,signature 被設定為 None (僅僅是一個雙重檢驗),pub_key 被設定為所參照輸出的 pub_key_hash。現在,除了當前交易,其他所有交易都是“空的”,也就是說他們的 signature 和 pub_key 欄位被設定為 None。因此,輸入是被分開簽名的,儘管這對於我們的應用並不十分緊要,但是位元幣允許交易包含參照了不同地址的輸入。

tx_copy.set_id()
tx_copy.vins[in_id].pub_key = None

hash 方法對交易進行序列化,並使用 SHA-256 演演算法進行雜湊。雜湊後的結果就是我們要簽名的資料。在獲取完雜湊,我們應該重置 pub_key 欄位,以便於它不會影響後面的迭代。

現在,關鍵點:

sk = ecdsa.SigningKey.from_string(
        binascii.a2b_hex(priv_key), curve=ecdsa.SECP256k1)
sign = sk.sign(tx_copy.txid.encode())
self.vins[in_id].signature = binascii.hexlify(sign).decode()

我們通過 priv_key 對 tx_copy.txid 進行簽名。一個 ECDSA 簽名就是一對數位,我們對這對數位連線起來,並儲存在輸入的 signature 欄位。
驗證方法與簽名是類似的:

def verify(self, prev_txs):
    tx_copy = self._trimmed_copy()
    for in_id, vin in enumerate(self.vins):
        prev_tx = prev_txs.get(vin.txid, None)
        if not prev_tx:
            raise ValueError('Previous transaction is error')
        tx_copy.vins[in_id].signature = None
        tx_copy.vins[in_id].pub_key = prev_tx.vouts[vin.vout].pub_key_hash
        tx_copy.set_id()
        tx_copy.vins[in_id].pub_key = None
        sign = binascii.unhexlify(self.vins[in_id].signature)
        vk = ecdsa.VerifyingKey.from_string(
            binascii.a2b_hex(vin.pub_key), curve=ecdsa.SECP256k1)
        if not vk.verify(sign, tx_copy.txid.encode()):
            return False
    return True

和上面類似,先進行副本的拷貝,然後置空signature,並重新計算id,然後再進行校驗

sign = binascii.unhexlify(self.vins[in_id].signature)
vk = ecdsa.VerifyingKey.from_string(
    binascii.a2b_hex(vin.pub_key), curve=ecdsa.SECP256k1)
if not vk.verify(sign, tx_copy.txid.encode()):
    return False

如果校驗失敗就直接返回False。

在new_transaction方法中加入簽名:

def new_transaction(self, from_addr, to_addr, amount):
    tx = Transaction(inputs, outputs)
    tx.set_id()
    self.sign_transaction(tx, from_wallet.private_key)
    return tx
def mine(self, bc):
    pow = ProofOfWork(self)
    for tx in self._transactions:
        if not bc.verify_transaction(tx):
            raise TransactionVerifyError('transaction verify error')
    try:
        nonce, _ = pow.run()
    except NonceNotFoundError as e:
        print(e)
    self._block_header.nonce = nonce

在挖礦方法mine加入交易引數校驗。

測試:

$python3 cli.py createwallet
Your new address is 11oHh3J3jx4yjKppGjCXijVxxc6QZU8zr
$python3 cli.py printwallet
Wallet are:
LZTnHqJG4sSHXHY5Yx1gDqXv9LKc1586Pj
# 首先初始化
$python3 main.py
1HaccEKgfrng4Bkg9aqBkNKpqQUpVfk8tY 1wQMgVMac1oR9jDDUUBQncK9N1vuwXpoM
Mining a new block
Found nonce == 45ash_hex == 0467d7a2be1538feb4fa3b8e71f96a770406ca42e36e4190a6f3821266868d26
Block(_block_header=BlockHeader(timestamp='1551159892.7875812', hash_merkle_root='', prev_block_hash='', hash='de46120e80fc192d3c2b4f14cce2f45f92f17323200aefce970eb599122c6af0', nonce=None, height=0))
Block(_block_header=BlockHeader(timestamp='1551159893.1240094', hash_merkle_root='', prev_block_hash='', hash='5107a5e658671a652b3858c86f38753e6bcab2295715ceb97cae3411550b5fb8', nonce=45, height=1))
$python3 cli.py send --from 1HaccEKgfrng4Bkg9aqBkNKpqQUpVfk8tY --to 1wQMgVMac1oR9jDDUUBQncK9N1vuwXpoM --amount 10
Mining a new block
Found nonce == 5ash_hex == 0e9ed9e55ea570beeb7606acc626886fb75aa3830317790aa997f5363cf3e01a
send 10 from 1HaccEKgfrng4Bkg9aqBkNKpqQUpVfk8tY to 1wQMgVMac1oR9jDDUUBQncK9N1vuwXpoM

一切正常工作。

參考:

[1] address

[2] 本節完整實現原始碼

以上就是python區塊鏈地址的簡版實現的詳細內容,更多關於python區塊鏈地址的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com