说起智能合约, 往往想到的不是比特币, 而是以太坊等其他区块链. 其实比特币也是有智能合约的, 之所以比特币被称为可编程货币, 就是因为比特币的智能合约. 比特币区块是由一个又一个交易组成的, 而比特币的智能合约就存在于这些交易中. 交易有输出, 有输出. 在输入与输出中都含有一段脚本, 这两段存在于输入与输出中的脚本就是比特币中的智能合约.

接下来, 我们通过一个简单交易, 一个多签交易以及一个自定义的交易来了解一下比特币中的智能合约.

以下简单交易与多签交易中的例子, 取自mastering bitcoin.

一个简单交易

如下是真实存在于比特币区块链里的一个简单交易. 为了便于阐述, 假设这个交易是小帅在小强那里购买了一部iphone, 小帅支付给了小强0.0845个比特币.

交易内容如下:

{
    "version": 1,
    "locktime": 0,
    "vin": [
        {
            "txid": "7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18",
            "vout": 0,
            "scriptSig": "3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813[ ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf",
            "sequence": 4294967295
        }
    ],
    "vout": [
        {
            "value": 0.01500000,
            "scriptPubKey": "OP_DUP OP_HASH160 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG",
        },
        {
            "value": 0.08450000,
            "scriptPubKey": "OP_DUP OP_HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 OP_EQUALVERIFY OP_CHECKSIG"
        }
    ]
}

初看这个交易内容会一脸蒙逼, 到底是个什么玩意? 智能合约在哪里? 不要担心, 接下来我们一起一步一步解读一下这个交易.

首先小帅既然支付给了小强0.0845个比特币, 那么小帅的比特币在哪里呢?

比特币中的输入与输出, 表示币从哪里到了哪里. 每个输出里都有一个金额(value), 可以理解为就是这个输出所拥有的比特币数额. 每个输出里也都有一个脚本(scriptPubKey), 我们称输出里的这个脚本为解锁脚本, 解锁脚本可以理解为是一道题. 只要有人能解出输出里的这个题, 那么这个输出里的比特币就属于解出题的这个人.

所以小帅的比特币就在所有能够被小帅解出的交易输出里. 而小帅的比特币余额就是所有能能被小帅解出的交易输出中比特币金额的总和.

所以可以理解为一个又一个的交易所做就是把比特币从一个输出里转到另一个输出里. 而交易里输入的作用就是在解某个输出里的题.

回过头来看小帅支付给小强的这个交易的输入:

    "vin": [
        {
            "txid": "7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18",
            "vout": 0,
            "scriptSig": "3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813[ ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf",
            "sequence": 4294967295
        }
    ],

txid表示引用某个交易, vout代表所引用交易中的第几个输出, scriptSig是一段脚本, 我们称为解锁脚本, 可以理解为解题答案. 整个输入可以理解为用scriptSig中的解锁脚本去解交易txid中的第vout输出中的锁定脚本.

也就是小帅要用解锁脚本scriptSig去解某个交易的输出, 以证明自己拥有这个输出里的比特币.

这个输入中引用的交易txid:7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18的第vout:0输出如下:

 "vout": [
        {
            "value": 0.1,
            "scriptPubKey": "OP_DUP OP_HASH160  7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG"
        }
    ]

小帅证明了这个输出中的0.1个比特币属于自己并把其中的0.0845个比特币支付给了小强. 那么剩下的0.1-0.0845=0.0155到哪里去了?

回头再看小帅支付给小强的交易中的输出:

    "vout": [
        {
            "value": 0.01500000,
            "scriptPubKey": "OP_DUP OP_HASH160 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG",
        },
        {
            "value": 0.08450000,
            "scriptPubKey": "OP_DUP OP_HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 OP_EQUALVERIFY OP_CHECKSIG"
        }
    ]

我们看到除了支付给小强的0.0845外, 还有一个金额为0.015的输出. 仔细观察可以发现0.015这个输出中的scriptPubKey与0.1这个输出中的scriptPubKey相等. 也就是说交易输出里的0.015比特币又支付给了交易输入里所引用的交易输出. 也就是说这个0.015的输出其实是做为找零又支付给了小帅自己.

可我们发现, 输入与输出并不相等, 输入为0.1, 输出为0.0845+0.015=0.995, 差了个0.005. 而这0.005比特币作为交易费付给了矿工.

再梳理一遍:

  • 小帅要给小强支付比特币.
  • 比特币存在于交易输出里, 被锁定脚本锁定.
  • 小帅的比特币就在小帅能解锁的所有的交易输出里.
  • 小帅在自己所能解锁的输出里选择了一个数额为0.1比特币的交易输出.
  • 小帅提供自己的解锁脚本来证明0.1比特币属于自己.
  • 小帅把0.1比特币中的0.0845支付给了小强, 0.015找零给了自己, 0.005作为交易费给了矿工.

接下来咱们就一起解剖一下上述例子中的锁定脚本与解锁脚本.

在比特币中解锁脚本与锁定脚本是拼接在一起执行的, 如果返回结果为1, 则解锁成功, 否则为解锁失败. 如下是这个简单交易中解锁脚本与锁定脚本拼接在一起在示意图: image_1c120avjl2roqaika0v1gnm9.png-92.7kB 这个图中将解锁脚本(unlocking script)与锁定脚本(locking script)拼接在了一起. 其中用<>标示的是数据, 如<Sig><PubKHash>. 未用<>标示的是比特币提供的操作指令, 如DUP, HASH160. 操作指令就是用来操作数据的.

比特币脚本是一个基于栈的语言, 数据会被压进栈, 操作指令会取(pop)栈中元素进行操作并把结果再压进栈. 先举一个最最简单的锁定脚本与解锁脚本例子, 如下: 锁定脚本:3 OP_ADD 5 OP_EQUAL. 解锁脚本:2. 将解锁脚本与锁定脚本拼接:2 3 OP_ADD 5 OP_EQUAL. 脚本的执行过程如下: image_1c1258608n9e1hip1s2d1j0k1c929.png-162.5kB

  • 脚本中前两个元素2 3都是数据, 依次推进栈里.
  • 脚本中第三个元素ADD为操作指令, ADD操作指令会将之前推进栈里的两个元素pop出来进行求合, 并把结果返回栈里. 这时栈里只剩一个元素5
  • 脚本中第四个元素5是数据, 推进栈里. 这时候, 栈里有两个元素, 都是5.
  • 脚本中最后一个元素EQUAL是操作指令, EQUAL操作指令将栈里的两个元素, 也就是两个5, 取出进行比对, 如果相待则返回true(用数字1表示). 显然5与5相等, 解锁成功.

看完最最简单的例子后, 回过头来, 再看小帅支付给小强这个简单交易.

深入剖析之前, 需要先复习一下非对称加密:

  • 有一对密钥, 一个为公钥, 一个为私钥.
  • 公钥是公开的, 人人都能知道, 私钥是不公开.
  • 公钥进行加密, 私钥进行解密.
  • 私钥进行签名, 公钥进行验签. 比特币中并未用到非对称加密中的加密, 只用于了签名.

在这个简单交易里, 小帅用自己的解锁脚本去解一个数额为0.1比特币的输出, 证明这0.1比特币属于自己. 交易输入如下:

    "vin": [
        {
            "txid": "7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18",
            "vout": 0,
            "scriptSig": "3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813[ ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf",
            "sequence": 4294967295
        }
    ],

解锁脚本scriptSig为:

3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813[ ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf

第一串数字为小帅的签名, 第二串数字为小帅的公钥, 如下:

<小帅的签名-sig> <小帅的公钥-PubK>

交易输入所引用的输出, 如下:

 "vout": [
        {
            "value": 0.1,
            "scriptPubKey": "OP_DUP OP_HASH160  7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG"
        }
    ]

锁定脚本OP_DUP OP_HASH160 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG为: 中间那串数据为小帅公钥的hash, 如下:

OP_DUP OP_HASH160 <小帅的公钥hash-PubKHash> OP_EQUALVERIFY OP_CHECKSIG

把解锁脚本与锁定脚本拼在一起:

<小帅的签名-sig> <小帅的公钥-PubK> OP_DUP OP_HASH160 <小帅的公钥hash-PubKHash> OP_EQUALVERIFY OP_CHECKSIG

以下是这个脚本的执行过程, 图中<sig>就是小强的签名, <PubK>就是小强的公钥. <PubKHash>就是小强公钥的hash: image_1c1270cloe4v16v8b6fpb1rt99.png-160.4kB image_1c127125d1vj31q1o193pi2e1m9hm.png-257.3kB

过程描述如下:

  • 脚本的前两个元素<sig><PubK>为数据, 推进栈.
  • 脚本的第三个元素DUP为指令, DUP指令会把栈顶元素<PubK>复制一份并压进栈.
  • 脚本的第四个元素HASH160为指令, HASH160指令会把栈顶元素<PubK>进行hash160操作再压进栈.
  • 脚本的第五个元素<PubKHash>为数据, 压进栈.
  • 脚本的第六个元素EQUALVERIFY为指令, 会弹出两个元素进行对比. 如果相待继续执行, 如果不等脚本退出, 执行失败.
  • 现在栈里剩下<sig><PubK>两个元素, 脚本的最后一个元素CHECKSIG为指令, 就是对最后剩下的这两个元素进行判断, 用第一个元素<PubK>去验证<sig>是否正确, 也就是用公钥去验证签名.
  • 如果可以验证成功, 则证明小帅拥有这个输出里的这0.1个比特币.

这种类型的脚本或是说智能合约在比特币里最为常见. 我们把这种交易命名Pay-to-Public-Key-Hash (P2PKH), 中文译为支付给公钥.

除了这种合约, 还有一种复杂点的合约也比较常见, 叫Multisignature, 多签交易.

一个多签交易

了解了简单交易, 了解多签交易就简单了. 多签交易可以理解为把比特币支付给了一堆公钥地址, 需要这堆公钥地址中的某几个或全部提供签名才能解锁.

多签锁定脚本的一般形式如下:

M < Public Key 1 > < Public Key 2 > ... < Public Key N > N CHECKMULTISIG

需要提供锁定脚本里这N个公钥中的M个公钥所对应的私钥才能解锁脚本.

比如下面这个多签解锁脚本:

2 < Public Key A > < Public Key B > < Public Key C > 3 CHECKMULTISIG

需要要提供三个公钥中的两个公钥所对应的私钥才能解锁脚本. 比如:

< Signature B > < Signature C >

把解锁脚本与锁定脚本拼在一起

< Signature B > < Signature C > 2 < Public Key A > < Public Key B > < Public Key C > 3 CHECKMULTISIG

这个脚本把除了最后个元素CHECKMULTISIG为指令外其他都为数据. CHECKMULTISIG指令很强大, 它会判断解锁脚本中是否提供了锁定脚本里3个公钥中的2个公钥所对应的正确签名.

除了简单交易-支付给公钥与多签交易后, 其实还有一个目前广泛使用的更加强大的Pay-to-Script-Hash (P2SH), 支付给脚本.

虽然多签很强大, 但是使用起来并不方便:

  • 每次支付都需要收款方提供一下锁定脚本. 因为支付方需要在交易的输出里写入这个锁定脚本.
  • 因为其中包含了多个公钥, 锁定脚本往往很大, 导致交易大小很大, 导致交易费很高. 明明的收款方的原因, 却要由支付方来承担, 不合理.

这时Pay-to-Script-Hash (P2SH)支付给脚本方式的交易诞生了, 解决了上述的问题. 这个很牛逼.

P2SH之前的多签脚本: 锁定脚本:2 PubKey1 PubKey2 PubKey3 PubKey4 PubKey5 5 CHECKMULTISIG 解锁脚本:Sig1 Sig2

P2SH之后的多签脚本: 赎回脚本:2 PubKey1 PubKey2 PubKey3 PubKey4 PubKey5 5 CHECKMULTISIG 锁定脚本:HASH160 <赎回脚本的hash值> EQUAL 解锁脚本:Sig1 Sig2 <赎回脚本>

赎回脚本这里是我自己翻遍的. 英文为Redeem Script.

可以看出原来的锁定脚本现在变为赎回脚本放到了解锁脚本里, 而现在的锁定脚本只记录了赎回脚本也就是原来锁定脚本的hash值.这大大减轻了锁定脚本的大小.

把解锁脚本与锁定脚本拼接在一起: Sig1 Sig2 <赎回脚本> HASH160 <赎回脚本的hash值> EQUAL

执行过程:

  • 将Sig1, Sig2, 赎回脚本都是数据. 依次推进栈.
  • HASH160为指令. 从栈顶取出赎回脚本计算hash值, 将hash结果推进栈.
  • <赎回脚本的hash值>为数据, 推进栈.
  • 这时栈前两个元素都是赎回脚本的hash值.指令EQUAL取出这两个hash值, 并对比这两个值是否相等.
  • 如果相等, 则执行赎回脚本. 这时执行的逻辑和多签脚本中所说的逻辑一致.

解锁脚本与锁定脚本可以按照自己的需求来编写, 只要符合比特币的规则. 接下来看一个自定义的脚本或合约的例子.

一个自定义交易

设计这么一个脚本或是合约, 达到以下目的:

  • 只要有小帅, 小强, 小壮, 3个人中的2个人的签名, 可以解锁锁定脚本.
  • 或者只有小景的签名, 可以解锁脚本.

那么这个合约, 可以这样写:

IF
 2 <小帅公钥> <小强公钥> <小壮公钥> 3 CHECKMULTISIG
ELSE
 OP_DUP OP_HASH160 <小景公钥的hash> OP_EQUALVERIFY OP_CHECKSIG
ENDIF

如果小帅, 小强, 小壮想解锁脚本, 则解锁脚本如下:

<小帅签名> <小强签名> true

如果小景想解锁脚本, 则解锁脚本如下:

<小景签名> <小景公钥> false

这两个解锁脚本后面的truefalse是为了控制锁定脚本中的条件语句的. IF指令会取出栈顶元素, 如果为true, 则执行IF分支 如果为false则执行ELSE分支.