1. 程式人生 > >比特幣底層技術探祕

比特幣底層技術探祕

譯者注:本文介紹了比特幣的一些底層的技術,包括地址、P2P網路、建立和釋出交易等等。文章還給出了關鍵性的Python程式碼片段,通過這些程式碼可以構建出一個最小最基本的比特幣客戶端程式。以下是譯文。

比特幣真的很酷。當然,關於這項技術目前還存在著不少的爭議,包括:它是否是一項有用的技術,加密數字貨幣是否存在著泡沫,目前面臨的管理問題是否能夠得到解決。但是從純技術層面來說,神祕的Satoshi Nakamoto創造了這個引人注目的技術。

不幸的是,儘管可以找到很多資源站在較高的層次解釋了比特幣的工作原理,但卻沒有有關底層的資料。在我看來,如果你從一萬英尺的高度看的話,你只能夠憑感覺來意會了。

對於這麼一個新興的領域,我發現我自己非常渴望去了解比特幣的工作機制。幸運的是,因為比特幣本質上是分散的,並且是對等的,所以任何人都能夠開發出一款符合協議標準的客戶端。為了能夠更好地瞭解比特幣的工作原理,我決定開發一款屬於我自己的比特幣客戶端,可以向比特幣區塊鏈釋出交易。

這篇文章介紹了開發一個最小而又可用的比特幣客戶端的過程,它可以建立一筆交易並將其提交到比特幣對等網路上,以便讓它包含在區塊鏈中。如果你只是想讀一下原始程式碼,可以隨時檢視我的Github程式碼庫

地址的生成

要成為比特幣網路的一部分,必須要有一個可以傳送和接收資金的地址。比特幣使用了公鑰加密技術,而地址是從私鑰派生出來的公鑰的雜湊版本。令人吃驚的是,與大多數的公共金鑰加密技術不同,它的公共金鑰會一直保密存放,直到資金從這個地址傳送出去。

術語解釋:在比特幣中,客戶端使用的術語“錢包”表示的是地址集合。從協議層面來講,沒有錢包這個概念,只有地址

比特幣對其地址使用了橢圓曲線公鑰密碼技術。橢圓曲線加密技術與RSA一樣,用於從私鑰生成公鑰,但其佔用的空間更小。如果你有興趣瞭解一下這種加密技術背後的數學知識的話,那麼

Cloudflare上的一篇入門文章值得一讀。

從256位的私鑰開始,生成比特幣地址的過程如下圖所示:

bitcoin address generation

在Python中,我使用ecsda庫來實現橢圓曲線加密。以下程式碼片段展示了從一個相當重要(也相當不安全)的私鑰0xFEEDB0BDEADBEEF(前面補零以達到64或者256個十六進位制字串)來獲取公鑰的過程。如果你想在地址中儲存任何實際的值,那麼需要一種更安全的私鑰生成方法!

趣事:我最初使用0xFACEBEEF這個金鑰建立了一個地址,並向它傳送了0.0005比特幣。一個月後,有人偷了我的0.0005比特幣!我猜,有人肯定偶爾會用一些簡單或者通用的私鑰來訪問地址。你真的應該使用一些更合適的金鑰派生技術!

from
ecdsa import SECP256k1, SigningKey def get_private_key(hex_string): return bytes.fromhex(hex_string.zfill(64)) # 在十六進位制字串的前面補零以達到64個字元的長度 def get_public_key(private_key): # this returns the concatenated x and y coordinates for the supplied private address # the prepended 04 is used to signify that it's uncompressed return (bytes.fromhex("04") + SigningKey.from_string(private_key, curve=SECP256k1).verifying_key.to_string()) private_key = get_private_key("FEEDB0BDEADBEEF") public_key = get_public_key(private_key)

執行程式碼,獲取到私鑰(十六進位制):

0000000000000000000000000000000000000000000000000feedb0bdeadbeef

獲取到的公鑰(十六進位制):

04d077e18fd45c031e0d256d75dfa8c3c21c589a861c4c33b99e64cf613113fcff9fc9d90a9d81346bcac64d3c01e6e0ef0828543edad73c0e257b845812cc8d28

以0x04開頭的公鑰表明這是一個沒有經過壓縮的公鑰,這意味著ECDSA(橢圓曲線數字簽名演算法)中x和y軸座標簡單的關聯在一起。根據ECSDA的原理,如果你知道x值,那麼y值只能取兩個值,一個偶數和一個奇數。基於這個資訊,可以僅使用x中的一個值和y的極性來表達一個公鑰。這使得公鑰的大小從65位減少到33位,這個過程(和後續計算的地址)稱之為壓縮。對於壓縮後的公鑰,根據y的極性,將以0x02或0x03開頭。未壓縮的公鑰常用於比特幣,這也是我在這裡所使用的。

要從公鑰生成比特幣地址,公鑰先要計算sha256雜湊,然後再計算ripemd160雜湊。這種雙重雜湊提供了額外的安全層,ripemd160雜湊提供了sha256的256位雜湊之後的160位雜湊,這樣縮短了地址的長度。一個有趣的結果是,兩個不同的公鑰可以雜湊生成一個相同的地址!然而,對於2的160次方個不同的地址,這不太可能在短時間內發生。

import hashlib

def get_public_address(public_key):
    address = hashlib.sha256(public_key).digest()

    h = hashlib.new('ripemd160')
    h.update(address)
    address = h.digest()

    return address

public_address = get_public_address(public_key)

這將生成c8db639c24f6dc026378225e40459ba8a9e54d1a這個公共地址,這有時會被稱為雜湊160地址

如前所述,有一點比較有意思,從私鑰到公鑰的轉換以及從公鑰到公共地址的轉換都是單向轉換。如果你有一個地址,那麼找到關聯公鑰的唯一辦法就是解決SHA256雜湊問題。這與大多數的公鑰加密技術不同,那些機密技術中公鑰是公開的,而私鑰會隱藏起來。而在當前這個情況下,公鑰和私鑰都會隱藏起來,而只公佈地址(雜湊過的公鑰)。

隱藏公鑰是有原因的。雖然從公鑰計算得到相應的私鑰通常是不可行的,但是如果生成私鑰的方法已經被破解,那麼就能很容易地通過公鑰推斷出私鑰。在2013年,這種事情發生在了Android比特幣錢包的身上。 Android在隨機數的生成上有一個關鍵性的缺陷,它會開啟一個向量,攻擊者通過這個向量可以從公鑰找到私鑰。這也就是為什麼不鼓勵地址重用,因為要簽署交易,你就得公開公鑰。如果你在向某個地址傳送交易後不重用該地址,那你就無需擔心該地址的私鑰會暴露。

表示一個比特幣地址的標準方式是使用Base58Check進行編碼。該編碼只是地址的一種表示形式(因此可以被解碼/反推)。它生成類似於1661HxZpSy5jhcJ2k6av2dxuspa8aafDac這種形式的地址。 Base58Check編碼提供了一個較短的地址表示方法,並且還內建校驗和,可以檢測出錯誤的地址。幾乎在每個比特幣客戶端中,你看到的地址都是Base58Check編碼後的地址。 Base58Check還包含一個版本號,在下面的程式碼中我把它設定為0,這表示該地址是一個pubkey雜湊。

# 58 character alphabet used
BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'

def base58_encode(version, public_address):
    """
    獲取Base58Check編碼的字串
    請參閱:https://en.bitcoin.it/wiki/base58Check_encoding
    """
    version = bytes.fromhex(version)
    checksum = hashlib.sha256(hashlib.sha256(version + public_address).digest()).digest()[:4]
    payload = version + public_address + checksum

    result = int.from_bytes(payload, byteorder="big")

    print(result)

    # 計算前面的0的數量
    padding = len(payload) - len(payload.lstrip(b'\0'))
    encoded = []

    while result != 0:
        result, remainder = divmod(result, 58)
        encoded.append(BASE58_ALPHABET[remainder])

    return padding*"1" + "".join(encoded)[::-1]

bitcoin_address = base58_encode("00", public_address)

以上所有的程式碼展示了我從私鑰FEEDB0BDEADBEEF(前面需要填充零)到到比特幣地址KK2xni6gmTtdnSGRiuAf94jciFgRjDj7W的整個過程!

通過這個地址,我現在就可以來獲得比特幣了!為了把比特幣放入我的地址,我用澳元從btcmarkets購買了0.0045比特幣(在撰寫本文時約為11美元)。 使用btcmarket的交易門戶,我將其轉移到上面的地址,在此過程中會損失0.0005比特幣的交易費用。你可以在交易* 95855ba9f46c6936d7b5ee6733c81e715ac92199938ce30ac3e1214b8c2cd8d7*中的區塊鏈上看到這筆交易。

連線到P2P(點對點)網路

現在,我有了一個地址,而且上面還有一些比特幣,事情變得更有趣了。如果我想將比特幣傳送到別的地方,那麼就必須連線到比特幣的P2P網路上。

引導

當我第一次學習比特幣的時候,我發現了一個關鍵的問題:由於網路的分散性,網路上的節點是如何找到其他節點的?沒有中央控制點,比特幣客戶端是如何知道如何引導並與網路的其他節點進行互動的?

理論服從實踐,在最初的節點發現過程中是存在著極少數的集中控制器。一個新的節點尋找其他節點的方法在原理上就是通過DNS去查詢Bitcoin社群成員維護的“DNS種子”伺服器。

事實證明,DNS非常適合於引導客戶端,因為DNS協議基於UDP,輕量級,不太容易受到DDoS攻擊。IRC以前曾被用作引導的方法,但是因為容易受DDoS攻擊這個弱點而停止使用了。

種子DNS被硬編碼到Bitcoin的核心原始碼中,並由核心開發人員負責修改。

下面的Python程式碼首先連線到一個DNS種子,然後打印出一個可以連線的節點列表。使用socket庫,它基本上執行的是一個nslookup操作,然後返回從seed.bitcoin.sipa.be查詢得到IPv4地址結果中的第一個。

import socket

# 向bitcoin DNS伺服器傳送DNS請求來查詢節點
nodes = socket.getaddrinfo("seed.bitcoin.sipa.be", None)

# 選擇第一個節點
node = nodes[0][4][0]

查到的地址是208.67.251.126,這是一個友好的對端節點,我可以去連線這個地址了!

跟對端節點打招呼

各個節點之間是通過TCP來建立連線的。連線對端節點時,比特幣協議最開始的握手訊息是一個版本訊息。在節點交換版本訊息之後,才會接受其他訊息。

比特幣協議訊息在“Bitcoin開發人員參考手冊”中有詳細的記錄。使用開發人員參考手冊作為指南,可以在Python中建立version訊息,如下面的程式碼片段所示。 其中大多數的程式碼都是用於開啟與對端節點的連線。如果你對細節感興趣的話,可以檢視開發者參考手冊。

version = 70014
services = 1 # not a full node, cant provide any data
timestamp = int(time.time())
addr_recvservices = 1
addr_recvipaddress = socket.inet_pton(socket.AF_INET6, "::ffff:127.0.0.1") #ip address of receiving node in big endian
addr_recvport = 8333
addr_transservices = 1
addr_transipaddress = socket.inet_pton(socket.AF_INET6, "::ffff:127.0.0.1")
addr_transport = 8333
nonce = 0
user_agentbytes = 0
start_height = 329167
relay = 0

使用Python的struct庫,版本有效載荷資料可以打包成正確的格式,請特別注意一下資料的位元組順序和位元組寬度。將資料打包成正確的格式很重要,不然對端節點將無法理解收到的原始資料。

payload = struct.pack("<I", version)
payload += struct.pack("<Q", services)
payload += struct.pack("<Q", timestamp)
payload += struct.pack("<Q", addr_recvservices)
payload += struct.pack("16s", addr_recvipaddress)
payload += struct.pack(">H", addr_recvport)
payload += struct.pack("<Q", addr_transservices)
payload += struct.pack("16s", addr_transipaddress)
payload += struct.pack(">H", addr_transport)
payload += struct.pack("<Q", nonce)
payload += struct.pack("<H", user_agentbytes)
payload += struct.pack("<I", start_height)

再說一遍,可以在開發人員參考手冊中找到這些資料的說明。最後,在比特幣網路上傳輸的每個有效載荷都需要加上一個包頭,其中包含了有效載荷的長度、校驗和以及訊息型別。包頭還包含了魔術常數0xF9BEB4D9,它在所有主要的比特幣訊息中都有。以下函式返回一個帶有包頭的比特幣訊息。

def get_bitcoin_message(message_type, payload):
    header = struct.pack(">L", 0xF9BEB4D9)
    header += struct.pack("12s", bytes(message_type, 'utf-8'))
    header += struct.pack("<L", len(payload))
    header += hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]

    return header + payload

將資料打包成正確的格式,並新增包頭,然後傳送給對等節點!

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((node, 8333))
s.send(get_bitcoin_message("version", payload))
print(s.recv(1024))

比特幣協議要求在接收到版本訊息後,返回一個Verack確認訊息。 因為我正在構建的是一個微型的“為了興趣而做”的客戶端,並且因為即使我不按照協議這麼做的話,其他節點也不會認為我這個客戶端有什麼不同,所以我忽略了他們的版本資訊,並且沒有傳送響應訊息。在連線時傳送版本訊息足以讓我在後面能夠傳送更加有意義的訊息。

執行上面的程式碼會打印出以下內容。結果看起來很有希望,“Satoshi”和“Verack”是在訊息轉儲中看到的最好的單詞!因為如果我的版本訊息格式錯誤的話,對端根本就不會做出迴應。

b'\xf9\xbe\xb4\xd9version\x00\x00\x00\x00\x00f\x00\x00\x00\xf8\xdd\x9aL\x7f\x11\x01\x00\r\x00\x00\x00\x00\x00\x00\x00\xddR1Y\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xcb\xce\x1d\xfc\xe9j\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\xb8>*\[email protected]\x8e\x10/Satoshi:0.14.0/t)\x07\x00\x01\xf9\xbe\xb4\xd9verack\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00]\xf6\xe0\xe2'

比特幣交易

要轉移比特幣,必須向比特幣網路廣播這筆交易。

有一個很重要的概念需要知道,那就是比特幣地址的餘額僅由該地址可以支出的“未花費的交易輸出”(UTXO)的數量組成。當鮑勃向愛麗絲髮送比特幣時,他只是建立了一個UTXO,而Alice(而且只有Alice)可以以此來建立另一個UTXO,併發送比特幣。因此,比特幣地址的餘額是由可以轉移到另一個地址的比特幣的數量決定,而不是直接由比特幣的數量決定。

要強調的是,當有人說他們擁有X數量的比特幣時,他們的意思是說所有可以用來支付的UTXO的總和是價值X比特幣。區別很小,但是很重要,比特幣地址的餘額不直接記錄在某個地方,而是可以通過對可以支付的所有的UTXO進行求和來得到的。當我意識到這個的時候,我大大驚歎了一句:“哦,原來它是這樣工作的!”。

這樣做的一個副作用就是交易輸出可以是未花費的(UTXO),也可以是已花費的。不可能只花費某人為你花費的數量的一半,然後在以後花費剩餘的數量。對於收到的輸出,如果你只想要花費其中一小部分,那你可以傳送這一小部分給其他人,而將其餘部分發送給自己。其簡化版本如下圖所示。

bitcoin address generation

在建立交易輸出的時候,將同時建立一個鎖定條件,這將允許將來的某人通過所謂的交易指令碼來花費它。最常見的鎖定條件是:“要花費這個輸出,你需要證明你擁有與特定公共地址對應的私鑰”。這被稱為“支付公鑰雜湊”指令碼。然而,通過比特幣指令碼建立其他型別的條件也是可以的。例如,建立可以由任何一個擁有某個雜湊的人花費的交易輸出,或者建立任何人都可以花費的交易輸出。

通過指令碼,可以建立簡單的基於合同的交易。指令碼是一種基本的基於棧的語言,它包含了大量的操作,以此來檢查雜湊是否相等以及驗證簽名。指令碼並不是完整的圖靈機,它不支援任何迴圈功能。與之有競爭關係的加密數字貨幣以太坊(Ethereum)就是建立在這一點上,它擁有“智慧合同”,並具有圖靈機的完整語言。關於在加密貨幣中包含圖靈機完整語言的實用性、必要性和安全性方面有很多的爭論,但我還是把爭論留給其他人吧!

在標準術語中,比特幣交易由輸入和輸出組成。輸入是一個UTXO(當前正在花費的),輸出是一個新的UTXO。單個輸入可以有多個輸出,但輸入需要在交易中完全消耗。輸入剩餘物的任何一部分都是礦工的採礦費。

對於我這個客戶端,我希望能夠將以前從交易所轉移到的比特幣傳送到我的FEEDB0BDEADBEEF地址。使用與之前相同的過程,我使用私鑰BADCAFEFABC0FFEE生成了另外一個地址1QGNXLzGXhWTKF3HTSjuBMpQyUYFkWfgVC

建立原始交易

要建立一筆交易,首先是對“原始交易”進行打包,然後對原始交易進行簽名。開發人員參考手冊中詳細說明了交易的內容。下文將講述交易的構成元素,但這裡先說幾個注意事項:

  • 比特幣中常見的術語包括簽名指令碼和pubkey指令碼,我發現這有點混亂。簽名指令碼用於滿足我們要在交易中使用的UTXO的條件,而pubkey指令碼用於設定條件以滿足我們正在建立的UTXO的花費。簽名指令碼的另一個更好的名稱是解鎖指令碼,而pubkey指令碼的另一個更好名稱是鎖定指令碼。
  • 比特幣交易值在Satoshis中指定。Satoshi代表比特幣可分割的最小部分,是一個比特幣的十億分之一。

為了簡單起見,下面顯示的是一個輸出和一個輸入的交易。可以以相同的方式來建立具有多個輸入和輸出的更復雜的交易。

欄位 描述
Version 交易的版本 (當前是1)
Number of inputs 需要花費的輸入的數量
Transaction ID 需要花費地交易地源頭
Output number 需要花費的交易的輸出
Signature script length 簽名指令碼的長度(位元組)
Signature script 簽名指令碼
Sequence number 除非你要使用一個鎖定時間,否則總是0xffffffff
Number of outputs 需要建立的輸出的數量
Value 需要花費的Satoshis的數量
Pubkey script length pubkey指令碼的長度(位元組)
Pubkey script pubkey指令碼
Lock time 包含在區塊中的交易的最早時間/區塊號

忽略簽名指令碼和pubkey指令碼,我們可以很容易地看到原始交易中的其他欄位應該怎麼設定。要將我的FEEDB0BDEADBEEF地址中的資金髮送到我的BADCAFEFABC0FFEE地址,我們來看看交易所建立的這筆交易:

  • 交易ID為95855ba9f46c6936d7b5ee6733c81e715ac92199938ce30ac3e1214b8c2cd8d7
  • 傳送到我的地址的輸出是第二個輸出,輸出1(輸出編號從0開始)。
  • 輸出的數量為1,因為我想將FEEDB0BDEADBEEF中的所有內容傳送到BADCAFEFABC0FFEE
  • 值最大可以達到40萬的Satoshis。為了留出一些費用來,一定要將值設定地小於這個最大值。我允許有2萬的Satoshi作為費用,所以將值設定為38萬。
  • 鎖定時間將被設定為0,這樣可以在任何時候或區塊中包含交易。

對於我們的交易的Pubkey指令碼,我們使用了“支付Pubkey雜湊”(或p2pk)指令碼。該指令碼確保只有擁有公鑰的人才能夠使用所提供的比特幣地址來支付所建立的輸出,並且所提供的簽名已經由儲存相應私鑰的人來生成公鑰。

要解鎖已由p2pk指令碼鎖定的交易,使用者需要提供公鑰和原始交易的雜湊簽名。根據公鑰計算出雜湊值,並與指令碼建立的地址進行比較,並對所提供的公鑰進行簽名驗證。如果公鑰的雜湊值和地址相等,並且簽名通過驗證,則可以花費輸出了。

在比特幣指令碼的運算物件中,p2pk指令碼如下所示:

OP_DUP
OP_HASH160
<Length of address in bytes>
<Bitcoin address>
OP_EQUALVERIFY
OP_CHECKSIG

將運算物件轉換為值(可以在wiki上找到)並輸入公共地址(在Base58Check編碼之前)可以得到如下十六進位制形式的指令碼:

0x76
0xA9
0x14
0xFF33195EC053D6E58D5FD3CC67747D3E1C71B280
0x88
0xAC

對交易進行簽名

p2pk交易中的簽名指令碼有兩個單獨但關聯的用途:

  • 通過提供公鑰雜湊到UTXO已傳送的地址,指令碼對我們正在嘗試花費的UTXO進行校驗(解鎖)。
  • 指令碼還會給我們正在提交到網路的交易進行簽名,這樣就沒有人能夠在不使簽名失效的情況下修改交易了。

但是,原始交易包含了一個簽名指令碼,而這個簽名指令碼又應該包含原始交易!要解決這個雞和雞蛋的問題,需要在對交易簽名之前把我們在簽名指令碼中使用的UTXO的Pubkey指令碼放進去。據我所知,使用Pubkey作為佔位符似乎並沒有什麼原因,佔位符可以是任意資料。

在原始交易被雜湊之前,它還需要附加一個Hashtype值。最常見的Hashtype值是SIGHASH_ALL,它標識整個結構,使得輸入或輸出都不能被修改。這個Wiki頁面列出了其他雜湊型別,這些型別允許在交易簽名後對輸入和輸出的組合進行修改。

下面這個函式將原始交易的值放在一起,返回一個python字典。

def get_p2pkh_script(pub_key):
    """
    這是一個標準的“支付pubkey雜湊”指令碼
    """
    # 先是OP_DUP,然後是OP_HASH160,然後是20 bytes (pub地址的長度)
    script = bytes.fromhex("76a914")

    # 要支付的地址
    script += pub_key

    # OP_EQUALVERIFY,然後是OP_CHECKSIG
    script += bytes.fromhex("88ac")

    return script

def get_raw_transaction(from_addr, to_addr, transaction_hash, output_index, satoshis_spend):
    """
    獲取一個輸入對應一個輸出的交易的原始交易
    """
    transaction = {}
    transaction["version"] = 1
    transaction["num_inputs"] = 1

    # 交易的位元組序需要反過來:
    # https://bitcoin.org/en/developer-reference#hash-byte-order
    transaction["transaction_hash"] = bytes.fromhex(transaction_hash)[::-1]
    transaction["output_index"] = output_index

    # 臨時讓簽名指令碼成為老的pubkey指令碼,這個指令碼後面會被取代
    # 我假設之前的pubkey指令碼就是這裡的p2pkh指令碼
    transaction["sig_script_length"] = 25
    transaction["sig_script"] = get_p2pkh_script(from_addr)

    transaction["sequence"] = 0xffffffff
    transaction["num_outputs"] = 1
    transaction["satoshis"] = satoshis_spend
    transaction["pubkey_length"] = 25
    transaction["pubkey_script"] = get_p2pkh_script(to_addr)
    transaction["lock_time"] = 0
    transaction["hash_code_type"] = 1

    return transaction

使用以下值呼叫程式碼能創建出我所感興趣的原始交易。

private_key = address_utils.get_private_key("FEEDB0BDEADBEEF")
public_key = address_utils.get_public_key(private_key)
from_address = address_utils.get_public_address(public_key)
to_address = address_utils.get_public_address(address_utils.get_public_key(address_utils.get_private_key("BADCAFEFABC0FFEE")))

transaction_id = "95855ba9f46c6936d7b5ee6733c81e715ac92199938ce30ac3e1214b8c2cd8d7"  
satoshis = 380000
output_index = 1

raw = get_raw_transaction(from_address, to_address, transaction_id, output_index, satoshis)

在上面的程式碼中我使用了私鑰來生成to_address,這個看起來可能會讓人感到困惑。其實這只是為了方便,並能展示出如何找到to_address。在你和別人交易的時候,你需要問他們要公共地址,而不需要知道他們的私鑰。

為了能夠進行簽名,並最終將交易釋出到網上去,原始交易需要採用適當的手段進行打包。這個過程是在get_packed_transaction函式中實現的,我不會把程式碼複製到這裡,因為它本質上只是一些結構打包程式碼。 如果你感興趣的話,可以在我的Github程式碼庫的bitcoin_transaction_utils.py檔案中找到它。

我定義了一個生成簽名指令碼的函式。生成簽名指令碼後,應該替換掉佔位符簽名指令碼。

def get_transaction_signature(transaction, private_key):
    """
    獲得原始交易的簽名指令碼
    """
    packed_raw_transaction = get_packed_transaction(transaction)
    hash = hashlib.sha256(hashlib.sha256(packed_raw_transaction).digest()).digest()
    public_key = address_utils.get_public_key(private_key)
    key = SigningKey.from_string(private_key, curve=SECP256k1)
    signature = key.sign_digest(hash, sigencode=util.sigencode_der)
    signature += bytes.fromhex("01") #hash code type

    sigscript = struct.pack("<B", len(signature))
    sigscript += signature
    sigscript += struct.pack("<B", len(public_key))
    sigscript += public_key

    return sigscript

從本質上講,簽名指令碼的提供是為了證明我可以把輸出當做輸入來花費,這個簽名指令碼是我之前交易的pubkey指令碼的輸入。這個工作機制如下所示,這是從比特幣wiki上獲取的。從表格的第一行到下面的最後一行,每行都是指令碼的一個迭代。 這是用於支付pubkey雜湊pubkey指令碼,上文提到過這是一個最常見的指令碼。 它也是我正在建立的交易和我要履行的交易的指令碼。

指令碼 描述
signature
publicKey
OP_DUP
OP_HASH160
pubKeyHash
OP_EQUALVERIFY
OP_CHECKSIG
簽名指令碼中的signaturepublicKey合併到pubkey指令碼中。
signature
publicKey
OP_DUP
OP_HASH160
pubKeyHash
OP_EQUALVERIFY
OP_CHECKSIG
signaturepublicKey新增到棧中
signature
publicKey
publicKey
OP_HASH160
pubKeyHash
OP_EQUALVERIFY
OP_CHECKSIG
棧頂的元素(publicKey)被OP_DUP複製了一份
signature
publicKey
pubHashA
pubKeyHash
OP_EQUALVERIFY
OP_CHECKSIG
棧頂的元素(publicKey)被OP_HASH160計算雜湊,並把pubHashA壓入棧中。
signature
publicKey
pubHashA
pubKeyHash
OP_EQUALVERIFY
OP_CHECKSIG
pubKeyHash新增到棧中。
signature
publicKey
OP_CHECKSIG 檢查pubHashApubKeyHash是否相等,如果不相等,則中斷程式執行。
True - 根據提供的publicKey來檢查signature是否是有效的交易簽名雜湊。

如果提供的公鑰雜湊不是指令碼中的公鑰雜湊,或者提供的簽名與提供的公鑰不匹配,那麼這個指令碼就會執行失敗。這是為了確保只有擁有pubkey指令碼中地址的私鑰的人才能夠花費輸出。

你可以看到,這是我第一次提供公鑰。到目前為止,只有公共地址被公佈出來。在這裡提供公鑰是為了能夠驗證交易的簽名。

為了能在網路上進行傳輸,我們可以使用get_transaction_signature函式對交易進行簽名和打包了!這涉及到使用真實簽名指令碼替換佔位符簽名指令碼,並從交易中移除hash_code_type,如下所示。

signature = get_transaction_signature(raw, private_key )

raw["sig_script_length"] = len(signature)
raw["sig_script"] = signature
del raw["hash_code_type"]

transaction = get_packed_transaction(raw)

釋出交易

隨著交易打包和簽名的完成,下一步就是網路的事情了。通過使用本文之前在bitcoin_p2p_message_utils.py中定義的一些函式,下面的程式碼片段將訊息頭新增到待發送的資料上,並將其傳送給對端節點。如前所述,首先需要傳送一個版本訊息,以便能夠接受後續的訊息。

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((get_bitcoin_peer(), 8333))
s.send(get_bitcoin_message("version", get_version_payload())
s.send(get_bitcoin_message("tx", transaction)

傳送交易是最煩人的一部分。如果我傳送了一個結構或簽名錯誤的交易,則對端節點通常會刪除連線,或者更好一點,回覆一個包含錯誤資訊的訊息。類似於這樣的錯誤訊息(非常得煩人):“S值不需要這麼高”,這是由於使用sigencode_der的ECSDA編碼方法對交易雜湊進行簽名導致的。儘管簽名是有效的,但實際上比特幣礦工並不喜歡以允許網路垃圾郵件形式格式化的ECSDA簽名。這個問題的解決方案就是使用sigencode_der_canonize函式,該函式用於將簽名格式化為其他的格式。這是一個簡單但非常難除錯的問題!

不管怎麼樣,我終於讓程式執行起來了,看到我的交易進入了區塊鏈,我非常得興奮!當獲知我的這個小巧簡潔並且是純手工打造的交易將永遠成為比特幣賬戶的一部分的時候,心中的成就感油然而生。

transaction success

當我提交交易的時候,我的交易費用相對於中位數來說相當得低(我通過比特幣費用網站查到的),因此這花了礦工大約5個小時的時間來決定將其包含在一個區塊中。我通過檢視交易的確認次數來檢查這一點,這是對交易所涉及的區塊數量的度量。在寫這篇文章的時候,有190個確認。這意味著在我的交易的區塊之後,還有190個區塊。這可以相當安全地得到確認,因為需要對網路進行猛烈的攻擊才能重寫190個塊來刪除我的交易。

總結

我希望你能通過閱讀本文來對比特幣的工作原理有所瞭解。雖然這裡提供的大部分資訊並不是很實用,並且你通常只會使用某個客戶端來完成所有的操作,但是我認為更好地理解工作原理能夠讓你更好的瞭解客戶端內部發生的事情,並讓你對這項技術更有信心。

如果你想閱讀更詳細的程式碼,或者深入地研究這個示例,請檢視我的Github程式碼庫。在比特幣世界裡還有很多的探索空間,我只是提供了一個非常常見的比特幣的例子。那裡肯定還有更多更酷的功能,而不僅僅是在兩個地址之間轉移價值!我也沒有研究挖掘比特幣以及向區塊鏈新增交易的過程。

如果你看到這裡,你可能已經意識到,我轉移到1QGNXLzGXhWTKF3HTSjuBMpQyUYFkWfgVC的380000的Satoshi(或0.0038比特幣)能被任何人取走,因為本文中有該地址的私鑰。我非常感興趣地想知道多久之後這些比特幣會被轉移走,我希望大家能夠採用我這裡介紹的一些技巧來做到這一點。如果你剛剛將私鑰載入到錢包應用程式中,那麼我會鄙視你,但我不會阻止你!在撰寫本文時,這些比特幣價值約為10美元,但如果把比特幣“拿到月球去”,誰知道它值多少呢!

如果你正在嘗試比特幣這個玩意並在尋找一個地址來發送比特幣,或者如果你認為這篇文章有價值,能給你一些啟發的話,那麼我的地址18uKa5c9S84tkN1ktuG568CR23vmeU7F5H將很高興能收到任意數量的捐款!或者,如果你想告訴我某些地方有錯誤,我也很樂意能聽到。

更多的資源

如果你發現這篇文章很有趣,那麼可以檢視以下更多的資源:

  • 掌握比特幣是一本解釋比特幣技術細節的書。我沒有完整地閱讀這本書,但它包含了很多很有用的資訊。
  • Ken Sheriff的部落格是一個很好的資訊來源,擁有很多與本文相同主題的文章。很不幸,我在寫這篇文章的時候才發現這個部落格。如果在這篇文章中有你不明白的地方,那麼閱讀他的帖子將是一個很好的開始。
  • 安德斯·布朗沃思(Anders Brownworth)的夢幻般的blockchain visual 101視訊是學習區塊技術工作原理的絕佳資料。
  • 除非你是一個疼痛的受虐狂,否則我建議你不要從零開始,除非你為了學習而特意希望這麼做。pycoin庫是一個Python比特幣庫,會讓你少一些頭痛。
  • 為了減少自己的痛苦,可以玩玩Bitcoin testnet,而不是像我一樣使用主網路。
  • 最後,再說一遍,本文的相關程式碼可以在我的Github程式碼庫中找到。