比特币智能合约入门(2)- 高级语言 sCrypt 简介

    技术2023-07-24  81

    脚本之谜

    上一篇文章中我们简要介绍了比特币智能合约的运行基础,即比特币网络底层脚本语言 Script 及其虚拟机运行原理。相信不少第一次接触的同学看过之后都会有一些困惑:这玩意和大家通常熟悉的 Python 、JavaScript 之类的脚本语言长得怎么一点都不像?“骨骼如此惊奇”的脚本语言该如何学习和运用呢?带着这些问题,我们再来深入聊聊。

    为什么是 Script ?

    众所周知,比特币网络本质上是由很多矿工节点运行和维护的一套公共账本,所有节点都需要保证这个账本上的数据是合法准确的。换句话说,任何一笔交易都可以被任何一个节点广播和校验,矿工实际上要通过运行 Script 脚本,来检查新交易中提供的“钥匙”能否“打开”旧交易中的“锁”,以此验证交易的合法性。在校验过程中,Script 的优势体现在这两个方面:

    无需编译、执行高效:由于 Script 脚本就是一串字节码1,在虚拟机处理前不需要再次编译,直接按照顺序解释执行指令就能得到结果,从而保证了执行效率。

    设计安全、防止攻击:Script 一个非常大的特点是没有跳转指令,这可以防止有人恶意(或因疏忽)在脚本中写入死循环从而攻击矿工网络。这一点经常被人诟病为图灵不完备,无法实现很多功能。但实际这是一种误解,完全可以通过一些技术手段实现图灵完备(后续在这方面计划另写文章聊聊)。

    但是,Script 脚本对于开发者来说就不那么友好了。学习、开发和测试过程都有非常高的门槛,因为本质上它就是一种汇编语言。

    高级语言(sCrypt)vs 汇编语言(Script)

    相信大多数学过汇编语言的同学都有着相似的回忆:记得在学校里学过!也就仅此而已了,绝大多数人在实际工程中是不会再碰到它的。对于不太熟悉汇编语言的同学,你可能只需要知道它是计算机发明早期就诞生的一种程序开发语言,用它写程序通常非常复杂和耗时。

    正因为 Script 语言开发时像汇编语言一样低效, sCrypt 项目才应运而生。其目标是为开发者提供一个更加高级的开发语言以及熟悉的开发环境,从而提升整个研发效率。简单地讲,开发者使用 sCrypt 语言开发智能合约,再通过编译器将源代码转换为 Script 脚本语言并放入交易发送上链,最终由矿工确保合约代码正确运行。

    下面正式为大家介绍比特币智能合约高级开发语言: sCrypt(英文发音:ess crypt)。

    sCrypt 基本语法与结构

    sCrypt 的语法2在设计时就以尽量降低开发者学习成本为目标,所以整体上和一些其他高级语言(例如 Javascript、Solidity 等)会有许多相似之处,这也是有意为之。

    基本数据类型

    sCrypt 是强类型语言,基本数据类型包括:

    bool:布尔值,取值 true 或者 false;

    int: 带符号的整型数值;

    int a1 = 42; int a2 = -4242424242424242; int a3 = 55066263022277343669578718895168534326250603453777594175500187360389116729240; int a4 = 0xFF8C; bytes:以16进制表示的字节数组,包含在单引号内并且以字母 b 作为前缀; bytes b1 = b'ffee1234'; bytes b2 = b'414136d08c5ed2bf3ba048afe6dcaebafeffffffffffffffffffffffffffffff00';

    为了进一步改善类型安全,bytes 还可以转换为更加精确的子类型描述以便借助编译器找出或减少潜在代码错误,具体类型包括:

    PubKey:公钥类型 PubKey pubKey = PubKey(b'0200112233445566778899aabbccddeeffffeeddccbbaa99887766554433221100'); SigHashType:签名哈希类型 SigHashType s = SigHashType(b'01'); SigHashType s = SigHash.ALL | SigHash.ANYONECANPAY; Sig:DER 格式的签名类型, 其中包含了签名哈希类型值; Sig sig = Sig(b'3045022100b71be3f1dc001e0a1ad65ed84e7a5a0bfe48325f2146ca1d677cf15e96e8b80302206d74605e8234eae3d4980fcd7b2fdc1c5b9374f0ce71dea38707fccdbd28cf7e41'); Ripemd160:RIPEMD-160 哈希类型 Ripemd160 r = Ripemd160(b'0011223344556677889999887766554433221100'); Sha1:Sha1 哈希类型 Sha1 s = Sha1(b'0011223344556677889999887766554433221100'); Sha256:Sha256 哈希类型 Sha256 s = Sha256(b'00112233445566778899aabbccddeeffffeeddccbbaa99887766554433221100'); OpCodeType:操作码数值类型 OpCode.OP_ADD // b'93'

    属性变量(property)

    每个合约可以由若干个属性变量(即一般面向对象语言中的实例变量),在该合约的函数中可以通过 this 关键字访问。如:

    contract Test { int x; bool y; bytes z; ... }

    构造函数(constructor)

    每个合约至多有 1 个构造函数(如果没有会由编译器自动生成一个),一般用于初始化属性变量。如:

    contract Test { constructor(int x) { this.x = x; } ... }

    内置函数 (build-in functions)

    require(bool expr):该函数被调用时检查参数表达式 expr 的布尔值结果。如果为 true 则继续往下执行;反之则立即中断执行过程,合约执行失败。相当于C/C++,Java,和python里面的assert()。

    exit(bool status):该函数被调用时立即中断执行过程,参数 status 取值为 true 时合约执行成功;反之则合约执行失败。

    公共函数(public function)

    公共函数是外部调用合约的接口。函数体中包含的主要逻辑代码可视为锁定脚本;函数参数可视为对应的解锁脚本。矿工实际上就是校验这对组合的执行结果。

    这类函数的特殊性有以下几点:

    没有明确的 returns 类型声明和函数结尾的 return 语句,其隐形返回true;函数结尾必须为 require() 函数调用;只有当函数结束,所有运行中遇到的 require()均通过,脚本校验才算通过;其他情况均视为脚本校验失败,从而会导致交易失败。 contract Test { public function equal(int y) { require(this.valueOf(y) == this.x); } ... }

    非公共函数 (function)

    非公共函数可以看做是合约的私有函数,主要目的是封装内部逻辑及代码重用。定义时需要使用关键字 returns 来说明返回值的类型,如:

    contract Test { function valueOf(int x) returns(int) { return x; } ... }

    除了上述简要介绍的一些内容,包含其他一些语言特性的完整官方文档可以在这里查看(持续迭代更新中)。

    sCrypt 合约示例

    最后来看下这个完整的 sCrypt 合约(应该很容易理解了):

    contract Test { // 定义合约 int x; // 属性变量 constructor(int x) { // 定义构造函数 this.x = x; // 初始化属性变量值 } public function equal(int y) { // 定义公共函数,解锁参数 y 为 int 类型 require(this.valueOf(y) == this.x); // 调用 require 函数,只有参数表达式值为 true 时,合约才能执行成功 } function valueOf(int x) returns (int) { // 定义内部函数 return x; // 返回值 } }

    后面的文章里我会继续给大家介绍 sCrypt 相关的开发工具及一些更加复杂的合约设计与实现,有兴趣的同学请继续保持关注:)

    附录


    Script 操作码(Opcode)全集 ↩︎

    sCrypt 官方文档 ↩︎

    Processed: 0.011, SQL: 10