需要了解的背景知識
首先要了解比特币的兩種腳本類型:
P2PKH(pay-to-public key-hash)和P2SH(pay-to-scrip-hash)
這部分可以在 <<精通比特币>>書中找到介紹,同時P2SH這個标準來源于比特币擴充協定BIP16,我之前翻譯過這個協定,有興趣可以看下:
http://blog.csdn.net/pony_maggie/article/details/77577121
另外需要了解下比特币腳本的執行原理,可以參考我以前寫的一篇部落格:
http://blog.csdn.net/pony_maggie/article/details/73656597
開始分析代碼
#輸入測試資料
inputs = [{
'output': 'cd6219ea108119dc62fce09698b649efde56eca7ce223a3315e8b431f6280ce7:0',
'value':
}]
#輸出測試資料
outputs = [
[{'address': addr0, 'value': }, {'address': addr1, 'value': }]
]
for outs in outputs:
mktx(inputs, outs)
inputs和outputs是測試資料,要用于比特币交易的輸入和輸出。
直接看mktx函數。函數建立一筆比特币交易的資料結構。用字典表示,序列化後傳回。先看下它傳回的結果:
e70c28f631b4e815333a22cea7ec56deef49b69896e0fc62dc198110ea1962cd0000000000ffffffff02e8030000000000001976a914d99f84267d1f90f3e870a5e9d2399918140be61d88acd00700000000000017a9140136d001619faba572df2ef3d193a57ad29122d98700000000
看着不太好了解,因為這是序列化之後的值,序列化之前是字元串形式的字典,可讀性好,如下:
{'locktime': , 'version': , 'outs': [{'value': , 'script': '76a914d99f84267d1f90f3e870a5e9d2399918140be61d88ac'}, {'value': , 'script': 'a9140136d001619faba572df2ef3d193a57ad29122d987'}], 'ins': [{'sequence': L, 'outpoint': {'index': , 'hash': 'cd6219ea108119dc62fce09698b649efde56eca7ce223a3315e8b431f6280ce7'}, 'script': ''}]}
根據比特币的協定, 比特币的交易(tx)結構是這樣的,
tx消息描述一筆比特币交易
字段尺寸 | 描述 | 資料類型 | 說明 |
---|---|---|---|
4 | version | uint32_t | 交易資料格式版本 |
1+ | tx_in | var_int | 交易的輸入數 |
41+ | tx_in | tx_in[] | 對前一輸出的引用 |
1+ | tx_out count | var_int | 交易的輸出數 |
8+ | tx_out | tx_out[] | 交易輸出或比特币去向清單 |
4 | lock_time | uint32_t | 鎖定交易的期限或block數目。如果為0則交易一直被鎖定。未鎖定的交易不可包含在block中,并可以在過期前修改(目前bitcon不允許更改交易,是以沒有用) |
和上面的結果對應下,除了輸入數和輸出數都可以對應上,兩個數目之是以沒有是因為可以根據list長度自動計算。
進入mktxs内部剝繭抽絲,來看看究竟是如何組裝一筆交易的。
def mktx(*args):
# [in0, in1...],[out0, out1...] or in0, in1 ... out0 out1 ...
ins, outs = [], []
for arg in args:
if isinstance(arg, list):
for a in arg:
(ins if is_inp(a) else outs).append(a)
else:
(ins if is_inp(arg) else outs).append(arg)
# 字典表示交易對象,初始化locktime等字段
txobj = {"locktime": , "version": , "ins": [], "outs": []}
for i in ins:
if isinstance(i, dict) and "outpoint" in i:
txobj["ins"].append(i)
else:
if isinstance(i, dict) and "output" in i:
i = i["output"]
txobj["ins"].append({
"outpoint": {"hash": i[:], "index": int(i[:])},
"script": "",
"sequence":
})
for o in outs:
if isinstance(o, string_or_bytes_types):
addr = o[:o.find(':')]
val = int(o[o.find(':')+:])
o = {}
if re.match('^[0-9a-fA-F]*$', addr):
o["script"] = addr
else:
o["address"] = addr
o["value"] = val
outobj = {}
if "address" in o:
outobj["script"] = address_to_script(o["address"])
elif "script" in o:
outobj["script"] = o["script"]
else:
raise Exception("Could not find 'address' or 'script' in output.")
outobj["value"] = o["value"]
txobj["outs"].append(outobj)
return serialize(txobj);
函數不長,前面提到一個tx裡包含多個tx_in和tx_out,函數首先初始化一個tx對象,
然後主要的工作就是組裝tx_in和tx_out了。内容來源于我們的測試資料,作為參數傳遞過來。
tx_in和tx_out資料結構如下:
tx_out的構成:
字段尺寸 | 描述 | 資料類型 | 說明 |
---|---|---|---|
8 | value | uint64_t | 交易的比特币數量(機關是0.00000001) |
1+ | pk_script | var_int | pk_script的長度 |
? | pk_script | uchar[] | Usually contains the public key as a Bitcoin script setting up conditions to claim this output |
tx_in的構成:
字段尺寸 | 描述 | 資料類型 | 說明 |
---|---|---|---|
36 | previous_output | outpoint | 對前一輸出的引用 |
1+ | script length | var_int | signature script 的長度 |
? | signature script | uchar[] | 用于确認交易授權的計算腳本 |
4 | sequence | uint32_t | 發送者定義的交易版本,用于在交易被寫入block之前更改交易 |
outpoint是對前一輸出的引用。
OutPoint結構的構成:
字段尺寸 | 描述 | 資料類型 | 說明 |
---|---|---|---|
32 | hash | char[32] | 引用的交易的散列 |
4 | index | uint32_t | 指定輸出的索引,第一筆輸出的索引是0,以此類推 |
我列印的txobj[‘ins’]和txobj[‘outs’]如下:
txobj[ins]:[{'sequence': L, 'outpoint': {'index': , 'hash': 'cd6219ea108119dc62fce09698b649efde56eca7ce223a3315e8b431f6280ce7'}, 'script': ''}]
txobj[outs]:[{'value': , 'script': '76a914d99f84267d1f90f3e870a5e9d2399918140be61d88ac'}, {'value': , 'script': 'a9140136d001619faba572df2ef3d193a57ad29122d987'}]
裡面有個函數address_to_script,需要說明下。
def address_to_script(addr):
if addr[] == '3' or addr[] == '2':
return mk_scripthash_script(addr)
else:
return mk_pubkey_script(addr)
從名字可以看出,該函數的功能時把位址轉換為腳本,到底是什麼意思呢?我們一步步來分析。
if判斷部分,比特币位址中’3’或者’2’開頭的是P2SH 位址(Pay-to-Script-Hash), 其它的位址按照P2PKH(Pay-to-Public-Key-Hash)方式處理。
P2PKH是比特币網絡中最常用的腳本形式,我們就以它為例進入mk_pubkey_script函數中看下:
def mk_pubkey_script(addr):
# Keep the auxiliary functions around for altcoins' sake
return '76a914' + b58check_to_hex(addr) + '88ac'
就一行實作。b58check_to_hex是base58解碼操作。我們知道比特币位址是這樣計算的:
A就是比特币位址,但是A并不是我們通常看到的比特币位址,我們平時看到那個是為了增加可讀性經過base58編碼的。
函數中的b58check_to_hex(addr)其實就是解碼轉回A。為啥要轉成A呢? 接續看,
76 a9 88 ac 是比特币腳本的四個指令對應的16進制編碼,分别表示
- 0x76:OP_DUP(複制棧頂元素)
-
0xa9:OP_HASH160(棧頂項進行兩次HASH,先用SHA-256,
再用RIPEMD-160)
-
0x88:OP_EQUALVERIFY(與OP_EQUAL 一樣,如結果為0,之後運作
OP_VERIFY)
-
0xac:OP_CHECKSIG (交易所用的簽名必須是哈希值和公鑰的
有效簽名,如果為真,則傳回1)
是以,mk_pubkey_script傳回的腳本是:
OP_DUP OP_HASH160 x14 A OP_EQUALVERIFY OP_CHECKSIG
如果你了解比特币腳本的運作機制,這個看起來就很眼熟了。沒錯這就是比特币UTXO中的鎖定腳本。而根據鎖定腳本的标準,A的位置放入的是16進制的比特币位址,也就是base58編碼之前的值。
看下實際運作的結果。比如這裡的示例,傳遞的參數是:
qgj1ThNfwLgHMp5qJUerYsuUEm8vHmVG
address_to_script輸出:
76a914d99f84267d1f90f3e870a5e9d2399918140be61d88ac