标准合约

多个合约

一个文件可以定义多个合约。在这种情况下,最后一个合约作为主合约并且被编译。其他合约是依赖项。

在下面这个例子中,标准的P2PKH合约被改写为两个其他合约:一个用来检查公钥和公钥哈希是否匹配的哈希谜题(hash puzzle)合约,还有一个检查签名和公钥是否匹配的Pay-to-PubKey(P2PK)合约。

contract HashPuzzle {
    Ripemd160 hash;

    public function spend(bytes preimage) {
        require(hash160(preimage) == this.hash);
    }
}

contract Pay2PubKey {
    PubKey pubKey;

    public function spend(Sig sig) {
        require(checkSig(sig, this.pubKey));
    }
}

contract Pay2PubKeyHash {
    Ripemd160 pubKeyHash;

    public function spend(Sig sig, PubKey pubKey) {
        HashPuzzle hp = new HashPuzzle(this.pubKeyHash);
        require(hp.spend(pubKey));

        Pay2PubKey p2pk = new Pay2PubKey(pubKey);
        require(p2pk.spend(sig));
    }
}

导入(import)

或者,可以将上述合约分到三个文件中。 Pay2PubKeyHash 合约 import 其他两个合约作为依赖。这就可以重用其他人写的合约,成为构建合约库的基础。

合约可以通过 new 来实例化。require 函数的参数是条件表达式, 调用合约的 public 函数可以作为条件表达式传入。

import "./hashPuzzle.scrypt";
import "./p2pk.scrypt";

contract Pay2PubKeyHash {
    Ripemd160 pubKeyHash;

    public function spend(Sig sig, PubKey pubKey) {
        HashPuzzle hp = new HashPuzzle(this.pubKeyHash);
        require(hp.spend(pubKey));

        Pay2PubKey p2pk = new Pay2PubKey(pubKey);
        require(p2pk.spend(sig));
    }
}

Library 库

库与合约相同,只是它不包含任何公共函数。它仅用于由合约或其他库导入。因此它不能被独立部署和调用。它经常用于对相关常量和静态函数进行分组。

library Util {
    // number of bytes to denote some numeric value
    static const int DataLen = 1;
    // number of bytes to denote length serialized state, including varint prefix (1 byte) + length (2 bytes), change length to 4 when you need PushData4
    static const int StateLen = 3;

    // convert signed integer `n` to unsigned integer of `l` bytes, in little endian
    static function toLEUnsigned(int n, int l): bytes {
        // one extra byte to accommodate possible negative sign byte
        bytes m = num2bin(n, l + 1);
        // remove sign byte
        return m[0 : len(m) - 1];
    }
}

标准合约

sCrypt自带标准库,里面定义了许多常用的合约。标准库是默认就导入的,不需要写 import 语句。

Utils

Utils 库提供了一组常用的实用函数,例如函数 Utils.fromLEUnsigned 将有符号整数 n 转换为小端字节序的无符号整数。函数 buildOutput(bytes outputScript, int outputSatoshis) : bytes 从其脚本和 satoshi 数量构建 一个 tx 输出。

以下示例显示了如何在 RabinSignature 合约的中使用标准库的 Utils 库。

library RabinSignature {
    static function checkSig(bytes msg, RabinSig sig, RabinPubKey pubKey) : bool {
        int h = Utils.fromLEUnsigned(hash(msg + sig.padding));
        return (sig.s * sig.s) % pubKey == h % pubKey;
    }

    static function hash(bytes x) : bytes {
        // expand into 512 bit hash
        bytes hx = sha256(x);
        int idx = len(hx) / 2;
        return sha256(hx[: idx]) + sha256(hx[idx :]);
    }
}

Tx

对比特币脚本的一个严重误解是,脚本只能访问锁定脚本以及对应的解锁脚本中提供的数据。因此,脚本的范围和能力被大大低估了。

sCrypt提供了一个强大的合约叫做 Tx。 它允许合约访问合约所在的 整个交易 ,包括锁定脚本和解锁脚本。我们把这种方法当成一个伪操作码 OP_PUSH_TX ,它可以把当前交易压到栈里,这样就可以在运行时访问了。更准确地说,可以访问的是在签名校验时用到的原像 preimage ,在 BIP143. 中有原像的详细定义。原像的数据格式如下:

  1. nVersion of the transaction (4-byte little endian)
  2. hashPrevouts (输入的输出点哈希 32字节哈希值)
  3. hashSequence (序列号哈希 32字节哈希值)
  4. outpoint (此输入的输出点 32字节哈希值 + 4字节小端)
  5. scriptCode of the input (输入对应的UTXO的锁定脚本)
  6. value of the output spent by this input (此输入对应的输出中包含的聪数 8字节小端)
  7. nSequence of the input (此输入的序列号 4字节小端)
  8. hashOutputs (输出的哈希 32字节哈希值)
  9. nLocktime of the transaction (交易的nLocktime 4字节小端)
  10. sighash type of the signature (签名类型 4字节小端)

例如,合约 CheckLockTimeVerify 确保合约中的币是时间锁定的,并且不能在时间达到 matureTime 之前花费,类似于 OP_CLTV

contract CheckLockTimeVerify {
    int matureTime;

    public function spend(SigHashPreimage txPreimage) {
        // using Tx.checkPreimage() to verify txPreimage
        require(Tx.checkPreimage(txPreimage));

        require(SigHash.nLocktime(txPreimage) >= this.matureTime);
    }
}

更多细节可以在这篇文章 OP_PUSH_TX 技术 中找到。要自定义 ECDSA 签名,例如选择 sighash 类型,有一个名为 Tx.checkPreimageSigHashType() 的版本,支持自定义 sighash 类型。要自定义临时密钥,有一个更通用的版本,称为 Tx.checkPreimageAdvanced()。请参阅 高级 OP_PUSH_TX 技术

原像的 ScriptCode 通常包含整个锁定脚本。唯一的例外是当其中有 OP_CODESEPARATOR (OCS) 时。在这种情况下,scriptCode 是锁定脚本,但在执行 OP_CHECKSIG 之前删除包括最后执行的 OCS 之前的所有内容。

OP_CODESEPARATOR

Tx 库提供了一组 OCS 版本方法来检查这个不包含完整锁定脚本的原像。在许多情况下,使用 OP_PUSH_TX 时不需要 scriptCode 或只需其一部分。 OCS 可以用来削减它的大小。例如,在下面的合约中,只需要整个原像的 nLocktime。我们使用 Tx.checkPreimageOCS(),传统的 Tx.checkPreimage() 的变体。唯一的区别是 OCS 是在前者的 OP_CHECKSIG 之前插入的。另请注意,我们将 Tx.checkPreimageOCS() 作为最后一条语句以达到最大优化效果。

contract CheckLockTimeVerifyOCS {
    int matureTime;

    public function unlock(SigHashPreimage preimage) {
        require(SigHash.nLocktime(preimage) > this.matureTime);
        require(Tx.checkPreimageOCS(preimage));
    }
}

SigHash

sCrypt 还提供了一个 SigHash 库来访问原像中的各个字段。例如,我们通常使用 SigHash.scriptCode 访问原像的 scriptCode 字段,使用 SigHash.value 访问原像的 value 字段,即在此合约中花费的比特币数量。

contract Clone {

    public function unlock(SigHashPreimage txPreimage) {
        require(Tx.checkPreimage(txPreimage));

        bytes scriptCode = SigHash.scriptCode(txPreimage);
        int satoshis = SigHash.value(txPreimage);
        bytes output = Utils.buildOutput(scriptCode, satoshis);
        require(hash256(output) == SigHash.hashOutputs(txPreimage));
    }
}

HashedMap

HashedMap 库提供了一种类似于哈希表的数据结构。唯一键和它们对应的值在存储之前被散列。 HashedMap 的大多数函数不仅需要一个键,还需要它的索引,按键的哈希升序排列。

构造函数

  • HashedMap(bytes data) 使用一些初始数据创建一个 HashedMap 实例。

    HashedMap<bytes, int> map = new HashedMap<bytes, int>(b'');
    // key and value types can be omitted
    HashedMap<int, bool> map1 = new HashedMap(b'');
    // key and value types cannot be omitted since they cannot be inferred
    auto map2 = new HashedMap<int, int>(b'');
    

SortedItem

SortedItem <T> 是一个通用结构体,它包含一个类型为 Titem 及 一个键索引 idx

struct SortedItem<T> {
  T item;
  int idx;
}

For most functions of HashedMap, a parameter named keyWithIdx of this type is required. It means that the key and its corresponding keyIndex should always be provided together.

实例方法

  • set(SortedItem <K> keyWithIdx, V val) : bool 使用 keyIndex 给定的键索引插入或更新 (key, val) 对。如果成功,则返回 true ;否则返回 false

    require(map.set({b'1234', 0}, 1)); // insert
    require(map.set({b'1234', 0}, 2)); // update it
    
  • canGet(SortedItem <K> keyWithIdx, V val): bool 检查是否存在 (key, val) 对, keyIndex 是键索引。如果成功,则返回 true ;否则返回 false

    require(map.canGet({b'1234', 0}, 2));
    
  • has(SortedItem <K> keyWithIdx) : bool 检查map中是否存在 key,其键索引为 keyIndex。如果两个条件都满足,则返回 true ;否则返回 false

    require(map.has({b'1234', 0}));
    
  • delete(SortedItem <K> keyWithIdx) : bool 删除给定 key 的条目,键索引是 keyIndex。如果成功,则返回 true;否则返回 false

    require(map.delete({b'1234', 0}));
    
  • clear() : bool 删除map的所有条目。

    map.clear();
    
  • size() : int 返回 HashedMap 的大小,比如它包含的键的数量。

    int s = map.size();
    
  • data() : bytes 返回 HashedMap 的序列化数据表示。

    bytes b = map.data();
    // this creates a deep copy of the map
    HashedMap<int, bool> mapCopy = new HashedMap(b);
    

HashedSet

HashedSet 库提供了一个类似集合的数据结构。它可以看作是一个特殊的 HashedMap,其中一个值与其键相同,因此被省略。唯一值在存储之前经过哈希处理。 HashedSet 的大多数函数都需要一个索引,按值的 sha256 哈希值升序排列。与 HashedMap 类似,这些函数也使用 SortedItem 类型参数。

构造函数

  • HashedSet(bytes data) 使用初始数据 data 创建一个 HashedSet 实例。

    struct ST {
      int x;
      bool y;
    }
    
    HashedSet<ST> set = new HashedSet<ST>(b'');
    // key and value types can be omitted
    HashedSet<ST> set1 = new HashedSet(b'');
    // key and value types cannot be omitted since they cannot be inferred
    auto set2 = new HashedSet<ST>(b'');
    

实例方法

  • add(SortedItem <E> entryWithIdx) : bool 添加 entry 以使用 index 给出的键索引进行设置。如果成功,则返回 true;否则返回 false

    require(set.add({b'1234', 0}));
    
  • has(SortedItem <E> entryWithIdx) : bool 检查集合中是否存在 entry 条目,其键索引为 index 。如果两个条件都满足,则返回 true;否则返回 false

    require(set.has({b'1234', 0}));
    
  • delete(SortedItem <E> entryWithIdx) : bool 删除给定 entry 条目,键索引为 index 。如果成功,则返回 true ;否则返回 false

    require(set.delete({b'1234', 0}));
    
  • clear() : bool 删除集合的所有条目。

    set.clear();
    
  • size() : int 返回集合的大小,即它包含的条目数。

    int s = set.size();
    
  • data() : bytes 返回集合的内部序列化数据。

    bytes b = set.data();
    // this creates a deep copy of the set
    HashedSet<ST> setCopy = new HashedSet(b);
    

Constants

sCrypt 在 Constants 库中定义了一些常用的常量值。你可以在代码中的任何位置使用这些常量。

library Constants {

    // number of bytes to denote input sequence
    static const int InputSeqLen = 4;
    // number of bytes to denote output value
    static const int OutputValueLen = 8;
    // number of bytes to denote a public key (compressed)
    static const int PubKeyLen = 33;
    // number of bytes to denote a public key hash
    static const int PubKeyHashLen = 20;
    // number of bytes to denote a tx id
    static const int TxIdLen = 32;
    // number of bytes to denote a outpoint
    static const int OutpointLen = 36;
}

完整列表

合约 构造函数参数 公共函数
Utils None
toLEUnsigned(int n, int l) : bytes
fromLEUnsigned(bytes b) : int
readVarint(bytes b) : bytes
writeVarint(bytes b) : bytes
buildOutput(bytes outputScript, int outputSatoshis) : bytes
buildPublicKeyHashScript(PubKeyHash pubKeyHash) : bytes
buildOpreturnScript(bytes data) : bytes
isFirstCall(SigHashPreimage preimage) : bool // return whether is the first call of series public function calls in stateful contract
Tx None
checkPreimage(SigHashPreimage preimage) : bool
checkPreimageOpt(SigHashPreimage rawTx) : bool
checkPreimageOpt_(SigHashPreimage rawTx) : bool // set sigHashType in ASM
checkPreimageSigHashType(SigHashPreimage txPreimage, SigHashType sigHashType) : bool
checkPreimageAdvanced(SigHashPreimage rawTx, PrivKey privKey, PubKey pubKey, int inverseK, int r, bytes rBigEndian, SigHashType sigHashType) : bool
checkPreimageOCS(SigHashPreimage preimage) : bool
checkPreimageOptOCS(SigHashPreimage rawTx) : bool
checkPreimageOptOCS_(SigHashPreimage rawTx) : bool // set sigHashType in ASM
checkPreimageSigHashTypeOCS(SigHashPreimage txPreimage, SigHashType sigHashType) : bool
checkPreimageAdvancedOCS(SigHashPreimage rawTx, PrivKey privKey, PubKey pubKey, int inverseK, int r, bytes rBigEndian, SigHashType sigHashType) : bool
SigHash None
nVersion(SigHashPreimage preimage) : bytes
hashPrevouts(SigHashPreimage preimage) : bytes
hashSequence(SigHashPreimage preimage) : bytes
outpoint(SigHashPreimage preimage) : bytes
scriptCode(SigHashPreimage preimage) : bytes
valueRaw(SigHashPreimage preimage) : bytes
value(SigHashPreimage preimage) : int
nSequenceRaw(SigHashPreimage preimage) : bytes
nSequence(SigHashPreimage preimage) : int
hashOutputs(SigHashPreimage preimage) : bytes
nLocktimeRaw(SigHashPreimage preimage) : bytes
nLocktime(SigHashPreimage preimage) : int
sigHashType(SigHashPreimage preimage) : SigHashType
HashedMap<K, V> bytes data
set(SortedItem<K> keyWithIdx, V val) : bool
canGet(SortedItem<K> keyWithIdx, V val) : bool
delete(SortedItem<K> keyWithIdx) : bool
has(SortedItem<K> keyWithIdx) : bool
clear() : bool
size() : int
data() : bytes
HashedSet<V> bytes data
add(SortedItem<V> entryWithIdx) : bool
delete(SortedItem<V> entryWithIdx) : bool
has(SortedItem<V> entryWithIdx) : bool
clear() : bool
size() : int
data() : bytes