# 比特币脚本研究

## 原理

\
比特币脚本是一个类[FORTH](https://en.wikipedia.org/wiki/FORTH)的语言，它具有以下特点：

1. 脚本简单，由opcode和data组成
2. 图灵不完备，**没有循环**
3. 执行顺序是从左到右
4. 基于**栈**执行，遵循**后进先出**

<br>

## 应用

### 简单应用

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

1. 加法操作

```Plain
 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)压入栈中 |

<br>

2. 减法操作

```Plain
 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等描述脚本更多用法<br>

### 锁定脚本和解锁脚本

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

**P2PK(pay to public key)**

\
sigScript: \<sig>pkscript: \<pubKey> OP\_CHECKSIG完整脚本为

```Plain
<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。<br>

**P2PKH(pay to public key hash)**

\
sigScript: \<sig> \<pubKey>pkscript: OP\_DUP OP\_HASH160 \<pubKeyHash> OP\_EQUALVERIFY OP\_CHECKSIG完整脚本为

```Plain
<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都不输出任何值，如果不通过，则直接返回错误。<br>

**P2MS(pay to multisig)**

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

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

上面是个2-3签名的例子，开头的0是因为OP\_CHECKMULTISIG有一个bug，需要多输入一个额外的值，输入0是为了防止延展性攻击[BIP147](https://en.bitcoin.it/wiki/BIP_0147)<br>

**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\
则完整的脚本有两段

```Plain
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>判断执行结果是否通过

<br>

**P2SH-P2WPKH/P2SH-P2WSH**

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

**P2WPKH(pay to witness public key hash)**

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

**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升级<br>

**P2TR(Public Key Path Spending)**

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

**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，前面的元素为输入参数。<br>

### 铭文(inscriptions)

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

1. Commit承诺

Commit阶段需要提交基于reveal脚本作为tapleaf生成(见[BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#specification))的tweaked public key<br>

2. Reveal 揭露

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

```Plain
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，也可以分解为三个部分

```Plain
c1f2d00e1cce0b839d2e197679cac7a15152ec244338032c7767f72c1332d49a65675a92934efa5e4869b2432a3e00abfc6819367af43949c2edc693b32d23ac05

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

```

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