Equity 语言入门

Equity 简介

Equity 是一种高级语言,专门用来编写运行在 Bytom 上的合约程序。通过编写、部署 Equity 智能合约,可以对 Bytom 上的各类资产进行操作。

为方便下文的理解,在正式介绍 Equity 语言之前,首先对 Bytom 上的资产做简要介绍:

  • Bytom 采用 BUTXO 结构,即区块链上记录由多种不同类型的 UTXO 构成的账本。

  • 每一笔 UTXO 都有两个重要属性:asset_idamount,即 资产编号资产数量。一般将这里的 资产数量 称作 value,有时也用 value 抽象地指代一笔 UTXO。

  • 所有 value(UTXO) 都被其对应的合约程序 program 锁定,只有满足 program 中定义的条件的输入,才能解锁 program 锁定的 value

因此,用 Equity 编写智能合约,其目的就是 "描述用智能合约锁定哪些资产,以及定义在哪些条件下可以解锁指定的资产"。

合约(contract)

Equity 合约程序由一个用 contract 关键字定义的合约(contract)构成。合约的构成形式为:

  • contractContractName(parameters)locksvalue{clauses}

具体来讲:

  • ContractName 是标识符,代表合约的名称,在编写合约时自定义。

  • parameters 是合约的参数列表,这些参数的类型应当在 Equity 数据类型 的范围内。

  • value 是标识符,表示锁定在合约中的 value,编写合约时自定义。

  • clauses 是一个或多个条款函数(clause)组成的列表。

条款函数(clause)

条款函数(clause)描述一种解锁合约中的 value 的方法,以及所需的任何数据(有时还需要支付新的 value)。clause 的结构是:

  • clauseClauseName(parameters){statements}

或者这样:

  • clauseClauseName(parameters)requirespayments{statements}

具体来讲:

  • ClauseName 是标识符,表示 clause 的名称,编写合约时自定义。

  • parameters 是 clause 的参数列表,这些参数的类型应当在 Equity 数据类型 的范围内。

  • payments支付需求组成的列表,在 条款的支付需求 中详细讲解。

  • statements 由一个或多个语句组成。每个语句都应是 verifylockunlock 中的一种,在 语句 中详细讲解。

合约与条款函数的参数(Parameters)

合约和条款的参数需指明 名称(name)数据类型(type)。参数的写法是:

  • name:TypeName

有多个参数时的写法为:

  • name1:TypeName1,name2:TypeName2, ...

为了简洁,可以像这样合并相同类型的相邻参数:

  • name1,name2, ... :TypeName

所以这两种合约的声明是等价的:

  • contract LockWithMultiSig(key1: PublicKey, key2: PublicKey, key3: PublicKey)
  • contract LockWithMultiSig(key1, key2, key3: PublicKey)

可用的数据类型有:

  • IntegerAmountBooleanStringHashAssetPublicKeySignatureProgram

将在 Equity 数据类型 中介绍这些数据类型。

条款的支付需求(Required payments)

有时候,条款函数(clause)会要求支付一些其他的 value 才能解锁合约中已有的 value。例如用美元交易换取欧元的时候,就需要支付美元以解锁合约中的欧元。

在这种情况下,必须在 clause 中使用 requires 语法来为所需的 value 指定名称并指定其数量和资产类型,形式为:

  • clauseClauseName(parameters)requiresname:amountofasset

在这里,name 是标识符,表示支付的 value 的名称。amountAmount 数据类型的表达式 (expressions)。而 assetAsset 数据类型的表达式。

还有的 clause 需要支付两种或更多种的 value 才能解锁合约。这时,可以在 requires 后按这种格式来写:

  • ... requiresname1:amount1ofasset1,name2:amount2ofasset2, ...

语句 (Statements)

条款函数(clause)的主体部分包含一个或多个语句:

  • verify 语句用来验证表达式的结果是否为真。

  • unlock 语句用来解锁合约中锁定的 value。

  • lock 语句可以将原合约中的 value 以及支付给条款函数的 value 锁定至新的合约中。

Verify 语句 (Verify statements)

verify 语句的格式如下:

  • verifyexpression

expression(表达式)的值必须是布尔型(Boolean)。只有当每个 verify 语句的检测结果都为 true 时,cluase 才能被成功执行。

一些例子如下:

  • verify above(blockNumber) 检测当前块的区块高度是否高于 blockNumber
  • verify checkTxSig(key, sig) 检测给定的签名,是否与预先设定的公钥以及解锁合约的交易匹配。
  • verify newBid > currentBid 检测某个数值,是否(严格)大于另一个数值。

Unlock 语句 (Unlock statements)

Unlock 语句只有一种格式:

  • unlockvalue

其中,value 是在 contract 声明时、 locks 关键字后指定的、合约中锁定的 value 的名称。该语句释放了合约中的 value,使得 value 可被用于任何用途。即没有指定一个新的合约来锁定释放的 value。

Lock 语句 (Lock statements)

Lock 语句的格式如下:

  • lockvaluewithprogram

该语句用 program 锁定了 value (此处的 value 可以是合约中锁定的 value 的名称,也可以是向 clause 支付的其他 value)。program 处的表达式必须是 Program 类型。

Equity 数据类型

Equity 编译器支持以下数据类型:

  • integer - 整数类型

    • Amount: 资产的数量 (更确切地说,是一些以最小单位记的资产数量),范围是 0 ~ 2^63,类型为 Amount 的变量表示锁定在合约中的资产

    • Integer: 介于 2^-63 ~ 2^63 之间的整数

  • boolean - 布尔类型

    • Boolean: 值为 true 或 false
  • string - 字符串类型,以十六进制字节串的形式出现

    • String: 普通的字符串

    • Hash: 哈希得到的字符串

    • Asset: 资产编号

    • PublicKey: 公钥

    • Signature: 私钥签名后的消息

    • Program: 用字节形式表示的程序码

表达式 (Expressions)

Equity 支持很多种表达式,这些表达式可用于 verifylock 语句,也可以用在 clause 声明时的 requires 部分里。

本节的 expr 代表一段表达式(expression)。例如 expr1+expr2 就可以看作是 20 + 15 或更复杂的语句。

  • -expr : 对数学表达式取负值
  • ~expr : 对字节串(byte string)做按位翻转(invert the bits)

下面的每种表达式都使用数字型操作数(numeric operands)(即IntegerAmount类型),并且返回一个 Boolean 型的结果:

  • expr1>expr2 : 检测 expr1 是否大于 expr2
  • expr1<expr2 : 检测 expr1 是否小于 expr2
  • expr1>=expr2 : 检测 expr1 是否大于或等于 expr2
  • expr1<=expr2 : 检测 expr1 是否小于或等于 expr2
  • expr1==expr2 : 检测 expr1 是否等于 expr2
  • expr1!=expr2 : 检测 expr1 是否不等于 expr2

下面的表达式用于 byte strings,且返回值也是 byte strings:

  • expr1^expr2 : 得到两操作数按位异或(XOR)的结果
  • expr1|expr2 : 得到两操作数按位或(OR)的结果
  • expr1&expr2 : 得到两操作数按位与(AND)的结果

下面的表达式用于数值型(numeric)操作数(IntegerAmount),返回数值型的结果:

  • expr1+expr2 : 将两操作数相加
  • expr1-expr2 : 从 expr1 中减去 expr2
  • expr1*expr2 : 将两操作数相乘
  • expr1/expr2 : 用 expr1 除以 expr2
  • expr1%expr2 : 即 expr1expr2 取余
  • expr1<<expr2 : 将 expr1 按位左移 expr2
  • expr1>>expr2 : 将 expr1 按位右移 expr2

还有其他的表达式类型:

  • (expr) : 依然表示 expr 本身

  • expr(arguments) : 表示函数调用,传入的参数 arguments 是一些用逗号分隔的表达式,具体可查阅下面的 函数 (functions)

  • 单独出现的标识符表示变量本身

  • [exprs] : 表示一个列表(list),其中 exprs 是一些用逗号分隔的表达式(列表参数目前仅用于checkTxMultiSig)

  • - 开头的一串数字序列属于 Integer 类型

  • 单引号 '...' 之间的字节序列(bytes)表示一个字符串

  • 前缀 0x 后跟 2n 个十六进制数字,则表示 n 字节长的字符串 (注:每个十六进制数为4位,每个字符为8位)

函数 (Functions)

Equity 内置了一些函数,可用在 verify 语句和其他地方:

  • abs(n) 返回数字 n 的绝对值。
  • min(x, y) 返回 x 和 y 两数中较小的那个数的值。
  • max(x, y) 返回 x 和 y 两数中较大的那个数的值。
  • size(s) 传入一个任意类型的表达式,返回其以字节为单位时的长度,返回值类型是 Integer
  • concat(s1, s2) 对两个字符串做拼接,从而生成一个新字符串。
  • concatpush(s1, s2) 传入两个字符串 s1 和 s2,返回的结果是 s1 与一段 Bytom 虚拟机操作码(BVM opcodes) 的拼接,这段操作码的作用是将 s2 送入 Bytom 虚拟机的栈。此函数通常用于嵌套合约中。具体可参考 BVM 操作码
  • below(height) 传入 Integer 类型的值,返回 Boolean 型的值,判断当前区块高度是否低于参数 height,如果是则返回 true,否则返回 false。
  • above(height) 传入 Integer 类型的值,返回 Boolean 型的值,判断当前区块高度是否高于参数 height。
  • sha3(s) 传入 string 类的值,返回其 SHA3-256 的哈希值(返回值类型为Hash)。
  • sha256(s) 传入 string 类的值,返回其 SHA-256 的哈希值(返回值类型为Hash)。
  • checkTxSig(key, sig) 传入 PublicKeySignature,返回 Boolean 型的值,以检测签名 sig 是否与公钥 key 和解锁交易匹配。
  • checkTxMultiSig([key1, key2, ...], [sig1, sig2, ...]) 分别传入列表形式的 PublicKeysSignatures,返回 Boolean 型的值,以检测是否每个 sig 都与 key 以及解锁交易匹配。关于列表参数顺序的问题:并非所有公钥都需要有签名与其匹配,但所有签名都应有匹配的公钥,并且这些公钥/签名必须在各自的列表中按相同的顺序排列。

Equity 合约的规则 (Rules for contracts)

只有遵循以下规则的 Equity 合约才是有效的:

  • 标识符不能互相冲突。例如,clause 参数的名称不能与 contract 参数的名称相同。(但是,两个不同的 clause 可以使用相同的参数名,这不构成冲突)
  • 每个 contract 参数必须至少在一个 clause 中被使用。
  • 每个 clause 参数必须在 clause 内被使用。
  • 每个 clause 必须使用 lockunlock 语句处理合约中锁定的 value。
  • 每个 clause 还必须用 lock 语句处理所有的支付给 clause 的 value(如果有的话)。

示例 (Examples)

单公钥锁定的合约

下面这个合约叫做 LockWithPublicKey,是最简单的合约之一。通过阅读本文档,可以详细了解它是如何工作的。

contract LockWithPublicKey(pubKey: PublicKey) locks value {
  clause spend(sig: Signature) {
    verify checkTxSig(pubKey, sig)
    unlock value
  }
}

这个合约的名称是 LockWithPublicKey。它锁定了一笔资产,被称作 value。在锁定 value 的交易(即创建合约的交易)中必须为 LockWithPublicKey 指定合约参数,即pubKey

LockWithPublicKey 包含一个 clause,这意味着有一种(且只有一种)途径解锁 value:把 Signature 作为参数,调用 spend 条款函数。

spend 条款中的 verify 检查传入的 Signature(sig)是否与 pubKey 和尝试解锁 value 的交易匹配。如果检查成功,则 value 被解锁。

借贷合约

接下来是一个更具挑战性的例子:叫做 LoanCollateral

contract LoanCollateral(assetLoaned: Asset,
                        amountLoaned: Amount,
                        repaymentHeight: Integer,
                        lender: Program,
                        borrower: Program) locks collateral {
  clause repay() requires payment: amountLoaned of assetLoaned {
    lock payment with lender
    lock collateral with borrower
  }
  clause default() {
    verify above(repaymentHeight)
    lock collateral with lender
  }
}

合约的名称是 LoanCollateral。它锁定了一些被称为 collateral 的 value。在创建此合约的交易中,必须指定五个合约参数:assetLoanedamountLoanedrepaymentHeightlenderborrower

此合约包含两个 clause,也就是说,有两种途径来解锁 collateral

  • repay() 不需要传入数据,但需要支付与 amountLoaned 等量的资产 assetLoaned
  • default() 既不需要传入数据,也不需要支付资产

合约的含义是:贷方 lender 在借方 borrower 已出具抵押物 collateral 的情况下,将与 amountLoaned 等量的资产 assetLoaned 借给借方。如果贷款被偿还给贷方,则将抵押物退还给借方。但是,如果还款截止时间已过,则贷方有权为自己索取抵押物。

repay() 条款函数通过两个简单的 lock 语句将付款发送给贷方,且将抵押物发送给借方。回想一下,向区块链参与者发送 value,其实就是将 value 锁定至允许接收者解锁的 program。

default() 条款函数的 verify 语句确保在截止区块高度已过时,可以将抵押物 collateral 锁定给贷方 lender。需要注意的是,这一过程并不是在截止时间到达的时刻自动发生的。贷方(或某个人)必须构造一个调用 LoanCollateral 合约的 default() 条款的交易,才能解锁抵押物 collateral 并将其重新锁定给 lender