理解比特币脚本语言的运行
Script 是一种简单的脚本语言,也是比特币交易处理的核心。如果你曾经写过汇编代码,你会发现这篇文章非常容易理解——可能还很有趣——否则它可能是最具挑战性的之一。保持专注!
比特币脚本是一个计算机程序,作为程序员的你肯定知道程序是什么。程序接收输入,执行一段时间,然后返回输出。编程语言是我们编写计算机能理解的程序的工具,因为大多数语言都带有将人类友好的代码映射到 CPU 操作(也称为 操作码)的 编译器。
操作码包括内存操作、数学运算、循环、函数调用以及你在像 C 这样的过程式编程语言中找到的一切。它们构成了 CPU 的口语,即所谓的 机器代码。由于字节是计算机的首选语言,难怪操作码也是字节。因此,机器代码是一串表示将在 CPU 上执行的操作的字节。
考虑以下用高级编程语言如 C 编写的代码:
x = 0x23;
x += 0x4b;
x *= 0x1e;
现在假设你想在一个假想的小端 CPU 上编译并运行这段代码,该 CPU 具有一个 16 位内存单元(一个 寄存器)和以下操作码集:
操作码 | 编码 | V |
---|---|---|
SET(V) | ab V |
16-bit |
ADD(V) | ac V |
16-bit |
MUL(V) | ad V |
16-bit |
操作码解释:
这种 CPU 的编译器会生成以下 9 个字节的机器代码:
ab 23 00 ac 4b 00 ad 1e 00
它的解释如下:
23
。4b
加到寄存器中,现在是 23 + 4b = 6e
。1e
,得到 6e * 1e = ce4
。寄存器保存最终结果,即 ce4
。
大多数时候,我们需要用 变量 来跟踪复杂的程序状态。在 C 中,取决于变量是静态分配还是用 malloc
分配,它们存储在不同的内存布局中。虽然 malloc
分配的数据像一个非常大的数组中的元素一样访问,静态变量则被推入和弹出一个称为 栈 的项目堆。栈以 LIFO 方式(后进先出)操作,这意味着你推入的最后一个项目将是第一个弹出的。
考虑这个简单的函数:
int foo() {
/* 1 */
/* 2 */
uint8_t a = 0x12;
uint16_t b = 0xa4;
uint32_t c = 0x2a5e7;
/* 3 */
uint32_t d = a + b + c;
return d;
/* 4 */
}
栈最初是空的 (1):
然后,三个变量被推入 (2):
[12]
[12, a4 00]
[12, a4 00, e7 a5 02 00]
第四个变量被分配为其他变量的和并推入栈 (3):
[12, a4 00, e7 a5 02 00, 9d a6 02 00]
栈顶是返回值,并通过其他方式返回给函数调用者。每个临时栈变量在块结束时被弹出 (4),因为推入/弹出操作必须平衡,以便栈始终恢复到其初始状态:
[12, a4 00, e7 a5 02 00]
[12, a4 00]
[12]
[]
同样,比特币核心有自己的“虚拟处理器”来解释 Script 机器代码。Script 具有丰富的操作码集,但与像英特尔这样的完整 CPU 相比非常有限。关于 Script 的一些关键事实:
事实上,第一点意味着第二点。第三点意味着在 Script 中没有命名变量,你只是在栈上进行计算。通常,你推入的栈项目成为后续操作码的操作数。在脚本结束时,栈顶项目是返回值。
在介绍实际的脚本之前,让我们先列举一些操作码。完整的操作码集请查看比特币官方 wiki 页面。
以下操作码将数字 0-16 推入栈:
操作码 | 编码 |
---|---|
OP_0 |
00 |
OP_1 -OP_16 |
51 -60 |
按照惯例,OP_0
和 OP_1
也表示布尔值 OP_FALSE
(零)和 OP_TRUE
(非零)。
示例:
或:
栈的演变如下:
[]
[4]
[4, 7]
[4, 7, 0]
[4, 7, 0, 16]
返回值是栈顶项目,所以脚本返回 16。相当无意义,我知道,但这是一个开始。
提供了几个操作码来推送自定义数据。它们在操作数的大小上有所不同:
操作码 | 编码 | L(长度) | D(数据) |
---|---|---|---|
OP_PUSHDATA1 |
4c L D |
8-bit | L 字节 |
OP_PUSHDATA2 |
4d L D |
16-bit | L 字节 |
OP_PUSHDATA4 |
4e L D |
32-bit | L 字节 |
例如,如果你的数据长度可以存储为 8 位数字,那么 OP_PUSHDATA1
是你的最佳选择。看看这个:
4c 14 11 06 03 55 04 8a
0c 70 3e 63 2e 31 26 30
24 06 6c 95 20 30
第一个字节显然是 OP_PUSHDATA1
操作码,后跟一个 1 字节长度的 14
,即十进制 20。所以,接下来是 20 字节的数据。此指令的效果是将这些数据推入栈:
[11 06 03 55 04 8a 0c 70
3e 63 2e 31 26 30 24 06
6c 95 20 30]
确实——就像 varints 一样——,对于非常短的数据有一种特殊的编码。如果操作码在 01
和 4b
(包括)之间,它是一个推送数据操作,其中操作码本身就是字节长度:
操作码 | 编码 | L(长度) | D(数据) |
---|---|---|---|
L | L D | 01 -4b |
L 字节 |
例如,在字符串中:
操作码 07
表示要推送 7 字节的数据:
你学到了一些关于机器代码和操作码的知识。Script 是一种简单的低级语言,由矿工软件理解。Script 状态通过栈内存进行跟踪。
在下一篇文章中,我将向你展示一些不仅仅是推送数据的操作码。如果你喜欢这篇文章,请分享。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!