比特币脚本研究

原理

比特币脚本是一个类FORTH的语言,它具有以下特点:

  1. 脚本简单,由opcode和data组成

  2. 图灵不完备,没有循环

  3. 执行顺序是从左到右

  4. 基于执行,遵循后进先出

应用

简单应用

我们先通过几个的例子来描述一下脚本的简单执行:

  1. 加法操作

 5  4  OP_ADD 9 OP_EQUAL

上面的输出的结果为true,然后我们来描述一下具体每一步做了什么

Stack
Script
Description

Empty

5 4 OP_ADD 9 OP_EQUAL

5

4 OP_ADD 9 OP_EQUAL

把5压入栈中,栈顶为5

5 4

OP_ADD 9 OP_EQUAL

把4压入栈中,栈顶为4

9

9 OP_EQUAL

执行OP_ADD,取出栈顶的4和5作为输入,并且把结果9作为输出压回栈中

9 9

OP_EQUAL

把9压入栈中

1

执行OP_EQUA,取出栈顶的两个9,并且比较,如果相等则把输出的true(1)压入栈中

  1. 减法操作

 5 4 OP_SUB 1 OP_EQUAL

上面的输出结果为true,下面是执行过程的描述:

Stack
Script
Description

Empty

5 4 OP_SUB 1 OP_EQUAL

5

4 OP_SUB 1 OP_EQUAL

把5压入栈中,栈顶为5

5 4

OP_SUB 1 OP_EQUAL

把4压入栈中,栈顶为4

1

1 OP_EQUAL

执行SUB,取出栈顶的4和5作为输入,4是v0,5是v1,OP_SUB执行的结果是(v1 - v0) = 1。把结果1压入栈中

1 1

OP_EQUAL

把1压入栈中

1

执行OP_EQUA,取出栈顶的两个1,并且比较,如果相等则把输出的true(1)压入栈中

这里需要注意,一些opcode有前后执行顺序,必须严格按照顺序把数据压入栈中,例如算术运算OP_SUB,或者签名验证OP_CHECKSIG等,如果顺序错误会导致脚本执行失败。 通过上述例子我们可以简单的理解脚本的执行流程,后面会通过锁定脚本,解锁脚本及Witness等描述脚本更多用法

锁定脚本和解锁脚本

这里我们把类型分为隔离见证前(pre-segwit)和隔离见证后(segwit)的来做区分

P2PK(pay to public key)

sigScript: <sig>pkscript: <pubKey> OP_CHECKSIG完整脚本为

<sig> <pubKey> OP_CHECKSIG
Stack
Script
Description

Empty

<sig> <pubKey> OP_CHECKSIG

<sig>

<pubKey> OP_CHECKSIG

把<sig>签名压入栈中

<sig> <pubKey>

OP_CHECKSIG

把<pubKey>公钥压入栈中

1

执行OP_CHECKSIG,取出<pubKey>和<sig>。验证签名是否通过,如果通过则把结果1压入栈中

OP_CHECKSIG如果通过,则栈中仅剩1,则表示当前sigScript可以解锁pkscript。

P2PKH(pay to public key hash)

sigScript: <sig> <pubKey>pkscript: OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG完整脚本为

<sig> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
Stack
Script
Description

Empty

<sig> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG

<sig>

<pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG

把<sig>压入栈中

<sig> <pubKey>

OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG

把<pubKey>压入栈中

<sig> <pubKey> <pubKey>

OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG

执行OP_DUP,复制栈顶数据并把结果压入栈中

<sig> <pubKey> <pubKeyHash>

<pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG

执行OP_HASH160,取出栈顶元素<pubKey>并且执行RIPEMD160算法生成<pubKeyHash>并且压入栈

<sig> <pubKey> <pubKeyHash> <pubKeyHash>

OP_EQUALVERIFY OP_CHECKSIG

压入<pubKeyHash>

<sig> <pubKey>

OP_CHECKSIG

执行OP_EQUALVERIFY,取出栈顶两个元素并且比较,比较成功则继续执行后面的脚本,如果失败则直接终止

1

执行OP_CHECKSIG取出栈顶两个元素并且把结果放入

对比原始的P2PK多了一个对pubKey的校验:这里OP_EQUALVERIFY并不输出任何值,只是控制是否可以继续执行后面的脚本。所有的VERIFY结尾的opcode都不输出任何值,如果不通过,则直接返回错误。

P2MS(pay to multisig)

sigScript: 0 <sig1> <sig2>pkscript: 2 <pubKey1> <pubKey2> <pubKey3> 3 OP_CHECKMULTISIG完整的脚本为

0 <sig1> <sig2> 2 <pubKey1> <pubKey2> <pubKey3> 3 OP_CHECKMULTISIG

上面是个2-3签名的例子,开头的0是因为OP_CHECKMULTISIG有一个bug,需要多输入一个额外的值,输入0是为了防止延展性攻击BIP147

P2SH(pay to script hash)

sigScript: <script params> <redeem script>pkscript: OP_HASH160 <scriptHash> OP_EQUAL我们使用常见的P2SH使用的redeem script来处理Redeem script: 2 <pubKey1> <pubKey2> <pubKey3> 3 OP_CHECKMULTISIG 则完整的脚本有两段

0 <sig1> <sig2> 2 <pubKey1> <pubKey2> <pubKey3> 3 OP_CHECKMULTISIG

OP_HASH160 <scriptHash> OP_EQUAL

执行过程:

  1. 先执行OP_HASH160 <scriptHash> OP_EQUAL判断是否一致

  2. 再根据<script params> <redeem script>判断执行结果是否通过

P2SH-P2WPKH/P2SH-P2WSH

和P2SH的区别是使用了witness字段传入参数 pkscript: OP_HASH160 <scriptHash> OP_EQUALsigScript: 0 <pubKeyHash>|<redeemScriptHash>witness: <sig> <pubKey> | <scriptParam><redeemScript> 后面会介绍隔离见证后的应用

P2WPKH(pay to witness public key hash)

pkScript: 0 <pubKeyHash>sigScript: emptyWitness: <sig> <pubKey> 执行脚本过程等价于P2PKH,但是极大的压缩了发送方数据量(pkScript只保留版本和hash,sigScript保留为空),对于(未升级)旧的比特币节点可能认为任何人都可以解锁。对于升级后的脚本会判断segwit版本,例子中的0,然后再进行签名公钥校验

P2WSH(pay to witness script hash)

pkScript: 0 <scriptHash>sigScript: emptyWitness: <script params> <redeemScript> 执行脚本过程等价于P2SH,同样也是压缩了发送方数据量。这里注意,P2WPKH和P2WSH很容易就可以分辨出来,因为publicKeyHash使用的是RIPEMD160,所以只有20字节,对应pkScript是22字节,而scriptHash使用的是SHA256输出为32字节,对应pkScript是34字节。所以为了统一地址做了taproot升级

P2TR(Public Key Path Spending)

pkScript: 1 <tweaked public key>sigScript: emptyWitness: <schnorr-sig>注意,这里直接使用了tweaked public key,是一个转换后的public key,是32字节,然后witness里面必须是Schnorr的签名。

P2TR(Script Path Spending)

pkScript: 1 <tweaked public key>sigScript: emptyWitness: <script> <control-block> P2TR类型和P2WPKH/P2WSH类型的witness版本号分别为1和0,这样可以很容易区分。同时又改善了P2WPKH/P2WSH里面的Hash长度不一致的问题,统一改成了32字节的tweaked public key。结合MAST的使用,通过使用witness字段让脚本有更多的衍生用法。当witness元素大于等于2的时候,则使用的是Script Path Spending,最后一个元素则为control-block,倒数第二个元素为script,前面的元素为输入参数。

铭文(inscriptions)

Ordinals协议支持的一种将任意内容(图片、视频等文件)附加到单个sat的协议,可以将它们变成比特币原生的数字艺术品。简单的说Inscriptions是一种NFT。铭刻(inscribe)是通过将要铭刻的聪发送到交易中来完成的,该交易会在链上显示铭文内容。然后,此内容与那个聪建立联系,将其变成一个不可改变的数字艺术品,可以被追踪、转移、储存、购买、出售。 以一个铭文铸造的例子来描述一个图片类型的NFT如何生成整个铭文铸造过程分为两部分

  1. Commit承诺

Commit阶段需要提交基于reveal脚本作为tapleaf生成(见BIP340)的tweaked public key

  1. Reveal 揭露

Reveal阶段需要传入参数,脚本的内容及control-blockpkScript: 1 <tweaked public key>sigScript: emptywitness: <script params> <script> <control-block> pkScript中的tweaked public key在前置的commit交易中已经提交。我们重点关注witness提交的内容以一个铭文铸造交易62e6629dcc1d451e2a9f2ba407c306d88c3128a14703be4e3616988cf44d108e。 通过解析witness第一个字段可知witness一共有三个部分组成:第一个部分长度为64字节(参数,实际为接受账户的Schnorr签名)第二个部分是script部分,解析出来后

f2d00e1cce0b839d2e197679cac7a15152ec244338032c7767f72c1332d49a65
OP_CHECKSIG
0
OP_IF
6f7264
1
696d6167652f706e67
0
OP_PUSHDATA2 89504e470d0a1a0a0000000d494844520000001c0000001c0806000000720ddf94000000017352474200aece1ce90000000467414d410000b18f0bfc6105000000097048597300000ec300000ec301c76fa8640000016449444154484bbd91b14ac4401445172bbf616b89bd68a360b360a185850882b5d8080afee37e88da09fb0be3dc217778797b93bc595d036733b96f724f36592c524aff8a0cf7890c051f5d17ca669161c696616df119f7859061c617da6b05f7d80e890a7d5914d5b585bd5025add82edb5df1c1e0e6e5b29cef1feeea99eb08bebb20c30c6e585dad0a1459307f7e79aa0f65517d1519825c342504102aa9ec2332040121b0d290588699ef83c35119665c53a8a4aa570a5148ac08a899978eca800f5886c3178fcd28c33a2cfc7a7b1f14f2603637e34395d79a67b3aff475b329675beccba66684dfb10a3c5c5088cd2c53857e76737bbdb5a759787e79510a71f665c03e088484d94e42c29229ac14848495bcf9a83b2eb293b3532950b40bf3ca03618b14c484f9f7f3b14bebf57a408bf44ffe215e2d98125a519bd0d37f4b2b054ae029c231a90c417f931746c5ed421090faebdf09412fdd09d527c3bd91d20f7b2876a5701d37750000000049454e44ae426082
OP_ENDIF
0
OP_IF
6f7264
1
6170706c69636174696f6e2f6a736f6e3b636861727365743d7574662d38
0
OP_PUSHDATA2 7b2270223a22766f7264222c2276223a312c227479223a22696e7363222c22636f6c223a2266336137376134333830666239353263633734366338333863336537616238346464343764613566373964306235353737626465656133323463306461623539222c22696964223a224f726469526f636b73222c227075626c223a2231455337623370636a527a4667796969714c7278484b546e43686535364c4a485937222c226e6f6e6365223a343833362c22736967223a22473759674d314f4d4e566554535033776d2f67394649694c59474879794f396946415466483772334677747650597433456d6173727745617735365048644b366474396a7152786b6a784253387773723130594f5a4e513d227d
OP_ENDIF

脚本第一个数据为用户的公钥,传入签名参数和公钥后执行OP_CHECKSIG,如果通过则把true放到栈中,此时注意到后面推入了0(false),则后面的两段IF...ENDIF语句并不会执行(语句中间则为镌刻的NFT数据)。此时栈中仅有1,则脚本验证通过。 第三个部分为control-block,也可以分解为三个部分

c1f2d00e1cce0b839d2e197679cac7a15152ec244338032c7767f72c1332d49a65675a92934efa5e4869b2432a3e00abfc6819367af43949c2edc693b32d23ac05

c1: tapleaf版本号
f2d00e1cce0b839d2e197679cac7a15152ec244338032c7767f72c1332d49a65: 用户的公钥,tweaked public生成使用的P
675a92934efa5e4869b2432a3e00abfc6819367af43949c2edc693b32d23ac05: 脚本生成的tapLeaf hash

到这相当于通过commit和reveal的方式完成了对铭文的镌刻,并且发送给对应的用户。

最后更新于