比特币脚本语言 - 第 1 篇

理解比特币脚本语言的运行

在 GitHub 上查看 Demo 代码"交易处理中的脚本"

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

操作码解释:

  • SET 将寄存器加载为值 V。
  • ADD 将 V 加到寄存器中。
  • MUL 将寄存器乘以 V。

这种 CPU 的编译器会生成以下 9 个字节的机器代码:

ab 23 00 ac 4b 00 ad 1e 00

它的解释如下:

  1. 将寄存器加载为值 23
  2. 4b 加到寄存器中,现在是 23 + 4b = 6e
  3. 将寄存器乘以 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 机器代码。Script 具有丰富的操作码集,但与像英特尔这样的完整 CPU 相比非常有限。关于 Script 的一些关键事实:

  1. Script 没有循环。
  2. Script 需要终止。
  3. Script 内存访问是基于栈的。

事实上,第一点意味着第二点。第三点意味着在 Script 中没有命名变量,你只是在栈上进行计算。通常,你推入的栈项目成为后续操作码的操作数。在脚本结束时,栈顶项目是返回值。

在介绍实际的脚本之前,让我们先列举一些操作码。完整的操作码集请查看比特币官方 wiki 页面

常量

以下操作码将数字 0-16 推入栈:

操作码 编码
OP_0 00
OP_1-OP_16 51-60

按照惯例,OP_0OP_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 一样——,对于非常短的数据有一种特殊的编码。如果操作码在 014b(包括)之间,它是一个推送数据操作,其中操作码本身就是字节长度:

操作码 编码 L(长度) D(数据)
L L D 01-4b L 字节

例如,在字符串中:

操作码 07 表示要推送 7 字节的数据:

下一篇?

你学到了一些关于机器代码和操作码的知识。Script 是一种简单的低级语言,由矿工软件理解。Script 状态通过栈内存进行跟踪。

下一篇文章中,我将向你展示一些不仅仅是推送数据的操作码。如果你喜欢这篇文章,请分享。

我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~

点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Davide De Rosa
Davide De Rosa
江湖只有他的大名,没有他的介绍。