sCrypt 编程与使用 Javascript 或 Python 进行的传统编程不同,因为当将包含编译结果的交易提交到比特币网络时,脚本大小直接决定了它们的运行成本。因此,最终的脚本应该尽可能小,以节省交易费用¹。下面我们列出了一些技巧,供开发人员手动优化其 sCrypt 合约的脚本输出。
所有函数调用均通过内联实现。函数主体会被复制到调用它的位置。如果同一个函数在不同的地方多次调用,则可以通过合并它们来节省代码大小。在下面的示例中,将两个 Tx.checkPreimage()
调用合并为一个。
// 优化前
contract Unoptimized {
public function unlock0(SigHashPreimage txPreimage, bool arg1) {
require(Tx.checkPreimage(txPreimage));
require(arg1);
}
public function unlock1(SigHashPreimage txPreimage, int arg2) {
require(Tx.checkPreimage(txPreimage));
require(arg2 == 0);
}
}
// 优化后
contract Optimized {
// idx is 0 when calling the original unlock0, unlock1 otherwise
public function unlock(SigHashPreimage txPreimage, int idx, bool arg1, int arg2) {
require(Tx.checkPreimage(txPreimage));
require(idx == 0 ? arg1 : arg2 == 0);
}
}
Inline ASM 函数允许将原始比特币脚本直接嵌入 sCrypt 代码中,以实现更细粒度的控制。如果您知道自定义脚本比 sCrypt 生成的脚本短,则可以改用它。
// 优化前
contract P2PKH {
Ripemd160 pubKeyHash;
public function unlock(Sig sig, PubKey pubKey) {
require(hash160(pubKey) == this.pubKeyHash);
require(checkSig(sig, pubKey));
}
}
// 优化后
contract P2PKH {
public function unlock(Sig sig, PubKey pubKey) {
asm {
op_dup
op_hash160
$pubKeyHash
op_equalverify
op_checksig
}
}
}
构造函数通常仅使用其参数来初始化每个属性。如果是这种情况,请使用默认构造函数以优化脚本大小。
// 优化前
contract Unoptimized {
int x1;
bytes x2;
bool x3;
constructor(int x1, bytes x2, bool x3) {
this.x1 = x1;
this.x2 = x2;
this.x3 = x3;
}
public function equal(int y) {
// TODO
}
}
// 优化后
contract Optimized {
int x1;
bytes x2;
bool x3;
public function equal(int y) {
// TODO
}
}
比特币虚拟机(BVM)完全在堆栈上运行。与许多其他虚拟机不同,它没有其他类型的内存,例如寄存器或 RAM,因此其它虚拟机在内存上写入变量的复杂度是 O(1),而比特币虚拟机写入变量的复杂度是用 O(n),其中 n是变量在堆栈中的深度²。
// 优化前
function unoptimized(bool flag): int {
int i = 0;
if (flag) {
i = -1;
} else {
i = 1;
}
// ...
}
// 优化后
function optimized(bool flag): int {
int i = flag ? -1 : 1;
// ...
}
sCrypt 合约的静态属性是一种典型的情况,它们存储在堆栈的底部,因此更新它们的成本特别高。如果确实需要更改,请检查看是否可以将其设置为非静态属性。
循环中,使用归纳变量比使用递增的常规变量更加优化。
// 优化前
int i = 0;
loop (10) {
// use i
i++;
}
// 优化后
loop (10) : i {
// use i
}
[1] 当前,交易费用仅与脚本/事务的大小成正比,未来会将演变为考虑脚本的复杂性。因此,优化目标也有望在未来发展演变。
[2] 读取变量始终为 O(1)。