比特币系统的脚本(Script)——交易生成和验证的原理(第一部分)(初稿)*****

时间:2022-02-14 08:48:42

前言

在谈及脚本系统之前,先问一个似乎很简单的问题:比特币的“地址”是什么? 估计大多数人对此不屑一顾——这个问题太简单(弱智)了。比特币地址是一个日常使用非常频繁的东西。几乎在绝大多数交易都会用到比特币地址。对比特币稍有一点儿了解的人都可能会把Base58encode编码和RIPEMD160哈希算法挂在嘴边,懂一点技术的人更是能把比特币地址的计算的每一环节都细说无遗。 实际上,这样的认识只停留在表面上。以这样的理解,恐怕无法真正明白什么是比特币系统,从而严重低估这一创造性发明的价值,甚至有些人会认为”真正有价值的不是比特币,而是区块链”。   在解释”比特币地址”是什么之前,先要铺垫一下关于比特币“交易”的概念。在比特币系统的创立之初并没有“地址”这一说法,那时也照样可以交易。那么交易是如何实现的?

 

一、交易的实现和比特币地址的引入

比特币的交易实际上是不依赖地址的,至少用的并不是我们所看到的”地址“。比特币系统的交易依赖于脚本。 支付款项时,我们实际上是把支付的数额与接收者的“赎回脚本”绑定到一起。日后,接收者可以用自己的”签名脚本“来确认使用权。每一笔交易的实现所依赖的只是”脚本“。 以下是两种常见脚本的格式: “赎回脚本”:(输出脚本)

  • P2PKH(支付到公钥地址模式):

OP_DUP OP_HASH160 (0×14) [一个20字节的哈希值] OP_EQUALVERIFY OP_CHECKSIG

  • P2SH(支付到脚本模式,使用多重签名就需要用到这种模式):

OP_HASH160 (0×14)  [一个20字节的哈希值] OP_EQUAL “签名脚本”:(输入脚本)

  • P2PKH模式:

[签名的字节数][签名]0×01 [公钥的字节数] [公钥]

  • P2SH模式:m-n Multisig

0×00  [字节数]  [签名1] 0×01 …[字节数] [签名m] 0×01 [支付合同脚本的字节数]  [m]  [字节数][公钥1]… [字节数][公钥n][n] OP_CHECKSIGVERIFY   用巴比特的一比交易作为示例:http://blockmeta.com/tx/c8bc7cff08249ea5f9970e15be64259b0135b0b6e37f1f9f088a719508cbd8bc

c8bc7cff08249ea5f9970e15be64259b0135b0b6e37f1f9f088a719508cbd8bc (txid)(Fees: 0.0001 BTC) 5.83294119 BTC Time: 2015-05-05 15:21:27 Input Scripts(输入脚本——签名脚本) (第一个输入脚本为P2SH模式,由3Ae2TYfyHvwH11pUy6HaK7rBYn9GfGZ3Fk地址支出)
00 (OP_FALSE,约定为P2SH方式) 48 (第1个签名的字节数) 304502210080075aa29c42f8062f75cf6ab32004944417af974775581719008052c78719710220409fee54c6ddf2ca83e090077e443f95b427a63cc1ad87fca2625951b789d1c2 (第1个签名) 01 (约定所签名的数据为HASH_ALL(tx)) 49(第2个签名的字节数) 3046022100b61d8f206d17efd6db32dad106f754f231ee8a16882929b1eb39a58bfd36b39e022100c62cff92dd6fb22b373025fc9b87044cf1b33502acc9de707e5f54d1c8a042a7 (第2个签名) 01 (约定所签名的数据为HASH_ALL(tx)) 47 (支付合同脚本的字节数) 52 (OP_2, 表明m-n签名中的m = 2) 21  (第1个公钥的字节数) 0293baf0397588acc1aba056e868fd188dc0eea7554b45370aae862f9d2493a4c1 21  (第2个公钥的字节数) 020ab7517cf22a46b503ee8dcae7f9f109ec4cd19f0ab9d77c89c607554f3d5aa9(第一个公钥) 52(OP_2, 表明m-n签名中的n = 2, 说明这是一个2-2签名) ae(OP_CHECKSIGVERIFY)

(第二个输入脚本为P2SH模式,由17EFZ829NBT2WETLj3wJ5YUfXVaGckuUgs地址支出)

48 (签名的字节数)

304502203fe5f04a013512a4773414b25edc8c7915473dd5cf87bc73d28e1aaffdb4d14f022100e16156d526d1498f2cf5eb02d53e02f7fd5cf1dfdd25e4b032fdc5c59c9fd27b (签名) 01 (约定所签名的数据为HASH_ALL(tx)) 21 (公钥的字节数) 0203635e5c184951e14fcfecc83b15960594f4fceec729e09a4a517b0a03a7f4b9 (公钥)
Output Scripts (输出脚本——赎回脚本) (第一个输出脚本为P2PKH模式,支付到1BQpsoxUq7N5Hv57QCnzLBbZSHGtqafaFy地址上)
OP_DUP OP_HASH160 14 (字节数,0×14 = 20L, 网站在显示时省略了这一字节) 7232ca33e0797405a512fa872934cd922c812965 (20字节的哈希值) OP_EQUALVERIFY OP_CHECKSIG
(第二个输出脚本为P2PKH模式,找零回3Ae2TYfyHvwH11pUy6HaK7rBYn9GfGZ3Fk地址)
OP_HASH160 14 (字节数,0×14 = 20L, 网站在显示时省略了这一字节) 622854939d571b63df97f47e8302b700ab2932b6 (20字节的哈希值) OP_EQUAL

为了简化说明,我们先只分析P2PKH模式的交易。如果读者对P2SH感兴趣,可以在下面介绍了脚本原理后,自己分析一下P2SH是如何实现的。 此处暂且先把“交易是如何验证的”这个问题放在一边。从交易数据的格式中可以看出: P2PKH下,付款时需要通过“签名脚本”确认资产的使用权,同时需要知道接收方的“赎回脚本”才能正确付款。 普通用户恐怕无法直接记住并使用这些脚本。那么他们如何才能进行交易?   为了简化日常使用的交易过程,核心开发者们引入了”比特币地址“这一概念。通过一些协议约定,使得各种类型的钱包软件可以用某种通用的方式自动推算出所需的的脚本。 实际应用中,这些脚本是由钱包软件代替用户来实现的。 核心开发者们约定并正式发布的一种协议,规定了某种特定数据格式,以及由该格式的数据演算对应脚本的方法。这是一种独立与比特系系统内核之外的、约定俗成的规则,使得各种不同的钱包软件可以实现用某种通用的方式来收发款。

  • P2PKH地址的格式约定如下:

1. 前导字节(1个字节):prefix = 0×00,表示这是一个P2PKH地址; 2. 公钥的哈希值(20字节):用hash160算法——RIPEMD160(sha256(公钥)),将公钥转换为一个20字节的数据; 3. 校验码(4个字节):用hash256算法——SHA256(SHA256([0x00] [20字节的哈希值] )),取前4个字节作为校验码; 4. 生成文本格式的地址:用Base58编码由前三步所得到的25个字节的数据。 由于前导字节为0×00,生成的地址首字母为字符“1”。

  • P2SH地址的格式约定如下:

1. 前导字节(1个字节):prefix = 0×05,表示这是一个P2SH地址; 2. 合同脚本的哈希值(20字节):用hash160算法——RIPEMD160(sha256(合同脚本)),生成一个20字节的数据; 3. 校验码(4个字节):用hash256算法——SHA256(SHA256([0x00] [20字节的哈希值] )),取前4个字节作为校验码; 4. 生成文本格式的地址:用Base58编码由前三步所得到的25个字节的数据。 由于前导字节为0×05,生成的地址首字母为字符“3”。

  • 演算对应的脚本的方式:

根据前导字节,判断地址模式,并自动生成对应格式的脚本: P2PKH模式:

“赎回脚本”:OP_DUP OP_HASH160 [20字节的哈希值] OP_EQUALVERIFY OP_CHECKSIG “签名脚本”:[签名][签名消息的类型(1个字节)][公钥]

P2SH模式:

“赎回脚本”: OP_HASH160 [20字节的哈希值] OP_EQUAL “签名脚本”:[签名1 … 签名n][合同脚本]OP_CHECKSIGVERIFY 其中,,m-n多重签名的合同脚本(当n < 16时),m [公钥1…公钥n]n

也就是说,比特币的地址是一种规定了格式和脚本演算方式的约定,使得大家可以用某种通用的方法进行交易。(不使用地址实际上也仍然可以交易)但是,这些脚本到底有什么用?为什么要把交易过程变得这么复杂?

 

二、脚本的重要性

如果只是为了适应某种特定交易,那么没有必要使用脚本。假设只是实现收发款,那么,在比特币系统创立之初就把”赎回脚本“约定为公钥,把“签名脚本”约定为签名,就可以了。 中心式方式下,完全可以这样做,因为如果日后发现某种方式无法满足新的交易类型,或是发现某种方式存在安全隐患,那么推出一个升级包或补丁,或是另外发行一个新版本就可以解决。 然而,去中心方式下就行不通了。某种协议一旦确定,任何变更都需要提前经过讨论并取得共识,除非问题简单到一目了然,并且只有唯一的解决方法,那么取得共识几乎是无法实现的。以目前区块扩容这样一个简单的问题,即使大家确实知道扩容迫不及待,且知道20M的大小虽不是长久之计,但至少可以暂时缓解危机,但仍要拖到大约1年以后才能知道是否能最终取得共识。   估计比特币系统的设计者是为了使系统有机会处理无法预见到的交易模式,即使几十年或数百年以后,就算不修改协议,这一系统仍不会过时。故此引入了脚本系统以增加适应性。不过,这也增加了系统设计的难度。 脚本解释程序(或流程)必须在一开始就设计得足够简单,这样才能不出现bug,同时还要并近乎完美,能够有足够的灵活度适应各种未知需求。一旦脚本系统确定下来,任何变更都必须经过严格的测试并先达成共识,否则势必引起分叉。   这种设计对开发者能力的要求非常高。早期的开发者们设计的脚本系统并不完善,协议也表述得非常含糊(比如OP_ADD这样的加法指令,没有规定对整数溢出如何处理),为了系统的安全性,还增加了很多与比特币系统本身无关的额外限制(包括脚本字节数限制、指令限制等等,区块大小的限制是另一种额外限制),这使得脚本系统丧失了很大的灵活性。所以说,比特币系统目前仍处于实验期,在系统设计上还没有达到理想状态。(当协议足够完善、各种限制放开后,比特币系统才可以说渡过了实验期。当然,即使渡过了实验期也不表示比特币系统一定会成功,但毫无疑问的是,这一系统至少是一种有足够震撼力的创造)

 

三、脚本原理

比特币系统的脚本系统设计得非常简洁,稍有编程基础的人很容易理解。与用堆栈(stack)实现一个简单的计算器的方式非常类似。 比特币的脚本系统借助堆栈进行运算。验证交易时,脚本系统依次读取每个指令,并对堆栈进行操作。所有指令结束后,检查堆栈中的残留数据,如果均为TRUE,则交易有效,否则交易无效。

堆栈(stack)是一种实现后进先出(LIFO, Last In First Out)方式的数据结构,使用两种基本操作:压入(push)和弹出(pop),具体实现时通常还会有另一个常用操作:检视(peek)。最上面的元素称为栈顶(top)。 打个比方:往一个有一定高度的箱子里放书(箱子的宽度只能容纳一本书),放入书的动作是push,取出书的动作是pop。最的书一定是最被取的。拿起最上面的一本检视一下再放回原处称为peek。

脚本系统规定,指令(或运算符)均为1个字节,也就是说,最多只能有256种指令。 协议规定了执行每一个指令(OP)时要对堆栈(S)进行什么操作。比如: 假设S中依次push了如下下数据:(a, b, c, d, e) OP_ADD:加法指令。从S中依次pop出两个数据,这两个数据分别为e, d。计算f = e + d. 然后将f 压入S。此时S中的元素为(a, b, c, f) OP_DUP:复制栈顶数据。从S中peek出一个数据,此时数据为f, 复制一份f并压入堆栈。此时S中的元素为(a,b,c,f,f) OP_PUSHDATA1: 压入数据。将紧随OP指令后面的1个字节的数据(比如g)压入堆栈。此时S中的元素为(a,b,c,f,f, g)。   脚本系统协议中的每种指令名称及其用法请参考:https://en.bitcoin.it/wiki/Script 附件中有一个github的链接,附有一个比特币脚本解释器的示例(C语言),没经过严格测试,仅供参考。 下面用一个P2PKH的交易来说明一下脚本系统是如何运作的,数据取自第一节中所引用的交易。 21

输入脚本(签名脚本):48304502203fe5f04a013512a4773414b25edc8c7915473dd5cf87bc73d28e1aaffdb4d14f022100e16156d526d1498f2cf5eb02d53e02f7fd5cf1dfdd25e4b032fdc5c59c9fd27b01210203635e5c184951e14fcfecc83b15960594f4fceec729e09a4a517b0a03a7f4b9

每个数字的含义如下:

PUSHDATA 48
签名 (DER) sequence 30
length 45
integer 02
length 20
X 3fe5f04a013512a4773414b25edc8c7915473dd5cf87bc73d28e1aaffdb4d14f
integer 02
length 21
Y 00e16156d526d1498f2cf5eb02d53e02f7fd5cf1dfdd25e4b032fdc5c59c9fd27b
SIGHASH_ALL 01
PUSHDATA 41 21
公钥 type 02
X 03635e5c184951e14fcfecc83b15960594f4fceec729e09a4a517b0a03a7f4b9
Y
从区块链上找到该UTXO所对应的输出脚本(赎回脚本)为: (注意:不是这一笔交易中的输出脚本)
  • OP_DUP OP_HASH160 44524fa542897f46a9a0cccc27ccb91ba822b4b6 OP_EQUALVERIFY OP_CHECKSIG
76 a9  14 44524fa542897f46a9a0cccc27ccb91ba822b4b6 88 ac
OP_DUP 76
OP_HASH160 a9
PUSHDATA 14
公钥哈希值 44524fa542897f46a9a0cccc27ccb91ba822b4b6
OP_EQUALVERIFY 88
OP_CHECKSIG ac
注:指令中,凡是小于0x4b的数字均为PUSHDATA指令,这个数字同时代表打算压入堆栈的字节数。 验证一笔交易时,首先要读取输入脚本中的内容。 题外话:在早期脚本协议的约定中,输入脚本中只允许OP_PUSHDATA指令,这给后期推行P2SH方式制造了很大的麻烦,2012年的时候不得不冒着硬分叉的风险,在取得多数共识后强行引入了新协议,这样我们才有机会使用多重签名或智能合约这种模式。 输入脚本(签名脚本):script = 48 30 45 02 20 3f e5 f0 4a 01 35 12 a4 77 34 14 b2 5e dc 8c 79 15 47 3d d5 cf 87 bc 73 d2 8e 1a af fd b4 d1 4f 02 21 00 e1 61 56 d5 26 d1 49 8f 2c f5 eb 02 d5 3e 02 f7 fd 5c f1 df dd 25 e4 b0 32 fd c5 c5 9c 9f d2 7b 01 21 02 03 63 5e 5c 18 49 51 e1 4f cf ec c8 3b 15 96 05 94 f4 fc ee c7 29 e0 9a 4a 51 7b 0a 03 a7 f4 b9 流程: 令p = script; op = *p; (op = 0×48),这是PUSHDATA指令,p++, 将后面的72个字节数据压入堆栈S, p +=72. op = *p;  此时,op = 0×21,这也是PUSHDATA指令,p++, 将后面的33个字节数据压入堆栈S, p +=33. 此时p已移动至脚本末尾,输入脚本读取完毕,
堆栈 指令 操作
() 0×48 push后面72字节的数据(签名数据sig)
([sig]) 0×21 push后面33字节的数据(公钥数据pubkey)
([sig], [pubkey])    

接下来读取输出脚本(赎回脚本): script = 76 a9 14 72 32 ca 33 e0 79 74 05 a5 12 fa 87 29 34 cd 92 2c 81 29 65 88 ac

流程: 令p = script; op = *p; (op = 0×76),这是OP_DUP指令,复制一份栈顶元素至堆栈S。p++; op = *p;  (op = 0xa9),这也是OP_HASH160指令,从S中pop出一个数据,进行hash160运算,将运算结果压回S。p++; op = *p; (op = 0×14),这是PUSHDATA指令,p++, 将后面的20个字节数据压入堆栈S, p +=20; op = *p;  (op = 0×88),这也是OP_EQUALVERIFY指令,从S中依次pop出两个数据数据,比较这两个数据,如果不同,在交易验证失败。否则,p++; op = *p;  (op = 0xac),这也是OP_CHECKSIG指令,从S中依次pop出两个数据数据,根据[sig]数据末尾附加的类型(0×01)对原始交易数据进行哈系运算,M=hash256(tx_raw),检查ECDSA_checksig(M, [sig], [pubkey])的返回值,若结果为1,则将OP_TRUE压入堆栈,否则压入OP_FALSE. 此时p已移动至脚本末尾,全部脚本解析完毕。 检查堆栈中的元素,若为TRUE,则交易有效,否则交易无效。 (接上表)
堆栈 指令 操作
([sig], [pubkey]) OP_DUP 复制栈顶元素[pubkey]至堆栈S
([sig,[pubkey], [pubkey]) OP_HASH160 pop出一个元素,并进行hash160运算,将结果push回S
([sig,[pubkey], [hash160_pubkey])  0×14  push后面20字节的数据
 ([sig,[pubkey], [hash160_pubkey], [hash160])  OP_EQUALVERIFY  pop出2个元素,比较大小。若不相等,则标记交易为无效
  ([sig,[pubkey])  OP_CHECKSIG  pop出两个元素,验证签名,如成功,则压入TRUE;否则压入FALSE
 (TRUE)    

交易验证成功,

四、常见的交易脚本

五、自定义脚本

附录1:代码示例 (未完待续)   chehw 2015.5.12