如何编写合约
本节系统介绍 UTXO_Compiler 合约语言的完整语法。如果你更喜欢从例子入手,可以先看 教程一,再回来查阅本节作为参考。
1. 合约结构
每个 .ct 文件只能定义一个合约,基本骨架如下:
Contract 合约名:
# 结构体定义(可选,可有多个)
Struct 结构体名:
字段名: 类型
# 公有函数(至少一个,作为合约的花费入口)
def 函数名(参数: 类型):
...
# 私有辅助函数(可选,以下划线开头)
def _辅助函数名(参数: 类型):
...合约名、函数名、字段名均为标识符,规则与 Python 一致:字母或下划线开头,后跟字母、数字或下划线。
2. 数据类型
基础类型
| 类型 | 描述 | 字面量示例 |
|---|---|---|
int,number | 整数(BVM 大整数) | 0, 42, -100 |
string | 字节字符串 | "hello", "world" |
hex | 十六进制字节数组 | 0x1234, 0xdeadbeef |
bool | 布尔值 | 1(真), 0(假) |
address | 比特币 P2PKH 地址 | "1RainRzqJtJxHTngafpCejDLfYq2y4KBc" |
address 类型只支持标准 Base58 P2PKH 地址(以 1 开头,34 个字符)。hex 和 string 底层都是字节序列,区别仅在字面量书写形式。
定长 hex 类型
在结构体字段中,可以使用 hexN 声明固定字节长度的十六进制字段,常用于描述比特币交易的固定格式字段:
Struct TxInput:
txid: hex32 # 32 字节交易 ID
vout: hex4 # 4 字节输出索引
sequence: hex4 # 4 字节序列号数组类型
结构体字段支持固定长度数组 Type[N]:
Struct Transaction:
Inputs: TxInput[3] # 3 个输入
Outputs: TxOutput[3] # 3 个输出数组下标访问:tx.Inputs[0],下标一般为整型字面量;若下标为整形变量,则一般与结构体数组字段组合使用,取到对应的结构体字段。
vout = BinToNum(BVM.unlockingInput.Slice(32, 4)) #获取code在父交易中输出的位置
vout_copy = vout.Clone()
#取到code_data
code_data = pretx.Outputs[vout_copy].LockingScript.SuffixData.Clone()uint64[] 数组
除了结构体数组外,合约里也常用 uint64[](写作 uint64[N])来表达定长 64 位无符号整数数组,未使用时数组字段作为整体占据一个栈高,需要使用时通过 OP_ROLL 移动到栈顶,然后通过多次 OP_SPLIT 拆分为独立元素。
amount: uint64[6] = temp_data.Slice(3, 48)
ft_amount_tax = Push(0)
for i in Range(5, -1, -1):
ft_amount_tax = BinToNum(amount[i]) + ft_amount_tax使用要点:
uint64[N]中的N必须是编译期可确定的固定长度;- 元素访问使用
arr[i],索引建议配合Range按固定边界遍历; uint64元素按 8 字节处理,适合金额、计数器、索引等整数序列场景。
如果你需要在结构体中表达一组计数值,也可以直接声明:
Struct BatchData:
counts: uint64[4]内联匿名结构类型
临时使用的复合字段,可以不单独定义结构体,直接内联:
utxoData: {txid: hex32, vout: hex4, sequence: hex4}
utxoData = Push(BVM.unlockingInput)
vout = BinToNum(utxoData.vout)3. 变量
赋值
count = 10
result = Hash160(pubKey) # 将函数返回值绑定到变量局部变量(除数组和内联匿名结构类型外)可直接赋值使用,结构体字段和函数参数必须声明类型。
合约成员变量(self)
合约成员变量可直接使用,在编译期会被替换为字节码中的固定常量,可在公有函数和私有函数中多次读取,不受所有权限制:
Contract P2PKH:
def verify(sig: hex, pubKey: hex):
pubKey_copy = pubKey.Clone()
pubKeyHash = Hash160(pubKey_copy)
EqualVerify(pubKeyHash, self.pubKeyHash)
result = CheckSig(sig, pubKey)4. 运算符
算术运算
sum = a + b
diff = a - b
prod = a * b
quot = a / b # 整除取模没有对应运算符,使用内置函数 Mod(a, b)。
比较运算
a == b # 等于
a != b # 不等于
a < b # 小于
a > b # 大于
a <= b # 小于等于
a >= b # 大于等于比较运算返回整数 1(真)或 0(假),可直接用于 if 条件或 Return。
逻辑运算
合约语言没有 and / or / not 关键字,逻辑运算统一用内置函数:
ok = And(condition1, condition2) # 逻辑与
ok = Or(condition1, condition2) # 逻辑或
ok = Not(condition) # 逻辑非5. 控制流
条件语句
if amount > threshold:
CheckSigVerify(sig, pubKey)
else:
Return (1 == 0) # 拒绝if,else 分支需成对出现,多分支需嵌套 if:
if role == 1:
_handleBuyer(sig, pubKey)
else:
if role == 2:
_handleSeller(sig, pubKey)
else:
Return (1 == 0)循环语句
循环使用 for ... in Range(start, stop, step) 形式,语义类似 Python 的 range,但参数顺序是 (start, stop_exclusive, step):
# 从 2 递减到 0(包含 0)
for i in Range(2, -1, -1):
data = Cat(items[i], data)
# 从 0 递增到 2(包含 2)
for i in Range(0, 3, 1):
total = Add(total, values[i])循环次数在编译期必须确定,主要用于遍历固定大小的数组或执行固定次数的操作。
6. 函数
公有函数(花费入口)
不以 _ 开头的函数是公有函数,每个公有函数对应一条独立的合约"花费路径"。公有函数按声明顺序执行顺序:
Contract MultiPath:
# 路径一:持有者正常花费
def spend(sig: hex, pubKey: hex):
...
# 路径二:超时后退款
def refund(sig: hex, pubKey: hex):
...私有辅助函数
以 _ 开头的函数只能在合约内部调用,用于封装重复逻辑:
def _verifyOwner(sig: hex, pubKey: hex, expectedHash: hex):
pubKeyCopy = pubKey.Clone()
pubKeyHash = Hash160(pubKeyCopy)
EqualVerify(pubKeyHash, expectedHash)
CheckSigVerify(sig, pubKey)
def spend(sig: hex, pubKey: hex, ownerHash: hex):
...
_verifyOwner(sig, pubKey, ownerHash)
...
Return (1 == 1)返回语句
Return(大写)将表达式结果压入执行栈并生成"OP_RETURN":
Return (1 == 1) # 永远通过
Return (1 == 0) # 永远拒绝
Return CheckSig(sig, pubKey) # 签名验证结果作为返回值
Return And(cond1, cond2) # 组合条件小写 return 用于私有函数将值返回给调用方:
def _computeHash(data: hex):
result = Hash160(data)
return result7. 结构体
结构体描述复合数据的字节布局,通常对应交易数据中某一段的格式:
Struct Script:
SuffixData: string
PartialHash: string
Size: int
Struct Output:
Value: int
LockingScript: Script # 结构体可以嵌套
Struct PreTX:
VLIO: string
Inputs: Input[3]
UnlockingScriptHash: string
Outputs: Output[3]结构体字段访问通过 . 操作符,支持多级链式访问:
scriptSize = pretx.Outputs[0].LockingScript.Size注意:字段访问会消耗该字段的所有权。访问同一字段两次,需要在第一次前先
.Clone()。详见 所有权系统。
8. 解构赋值
{} 语法用于接收返回多个值的函数的结果,或初始化结构体:
# 接收 Split 的两个返回值
{header, body} = Split(rawData, 4)
# 接收私有函数的多返回值
{x, y} = _getCoords(encoded)
# 结构体字面量初始化(按字段顺序)
point: Point = {10, 20}9. 注释
使用 # 进行行注释:
# 这是一整行注释
count: int = 0 # 这是行尾注释目前不支持多行注释,较长的注释需每行都加 #。
下一步
- 如何部署与调用合约 — 编译输出与链上调用流程