pybitcointools源码分析之比特币交易数据结构

需要了解的背景知识

首先要了解比特币的两种脚本类型:

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': 158000
   }]

#输出测试数据
outputs = [
            [{'address': addr0, 'value': 1000}, {'address': addr1, 'value': 2000}]
]

for outs in outputs:
    mktx(inputs, outs)
            

inputs和outputs是测试数据,要用于比特币交易的输入和输出。

直接看mktx函数。函数创建一笔比特币交易的数据结构。用字典表示,序列化后返回。先看下它返回的结果:

0100000001e70c28f631b4e815333a22cea7ec56deef49b69896e0fc62dc198110ea1962cd0000000000ffffffff02e8030000000000001976a914d99f84267d1f90f3e870a5e9d2399918140be61d88acd00700000000000017a9140136d001619faba572df2ef3d193a57ad29122d98700000000

看着不太好理解,因为这是序列化之后的值,序列化之前是字符串形式的字典,可读性好,如下:

{'locktime': 0, 'version': 1, 'outs': [{'value': 1000, 'script': '76a914d99f84267d1f90f3e870a5e9d2399918140be61d88ac'}, {'value': 2000, 'script': 'a9140136d001619faba572df2ef3d193a57ad29122d987'}], 'ins': [{'sequence': 4294967295L, 'outpoint': {'index': 0, '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": 0, "version": 1, "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[:64], "index": int(i[65:])},
                "script": "",
                "sequence": 4294967295
            })


    for o in outs:
        if isinstance(o, string_or_bytes_types):
            addr = o[:o.find(':')]
            val = int(o[o.find(':')+1:])
            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对象,

txobj = {"locktime": 0, "version": 1, "ins": [], "outs": []}

然后主要的工作就是组装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': 4294967295L, 'outpoint': {'index': 0, 'hash': 'cd6219ea108119dc62fce09698b649efde56eca7ce223a3315e8b431f6280ce7'}, 'script': ''}]


txobj[outs]:[{'value': 1000, 'script': '76a914d99f84267d1f90f3e870a5e9d2399918140be61d88ac'}, {'value': 2000, 'script': 'a9140136d001619faba572df2ef3d193a57ad29122d987'}]

里面有个函数address_to_script,需要说明下。

def address_to_script(addr):
    if addr[0] == '3' or addr[0] == '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 = RIPEMD160(SHA256(K))

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 0x14 A OP_EQUALVERIFY  OP_CHECKSIG

如果你了解比特币脚本的运行机制,这个看起来就很眼熟了。没错这就是比特币UTXO中的锁定脚本。而根据锁定脚本的标准,A的位置放入的是16进制的比特币地址,也就是base58编码之前的值。

看下实际运行的结果。比如这里的示例,传递的参数是:

1Lqgj1ThNfwLgHMp5qJUerYsuUEm8vHmVG

address_to_script输出:

76a914d99f84267d1f90f3e870a5e9d2399918140be61d88ac
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容