How to Write a Contract
This section systematically introduces the complete syntax of the UTXO_Compiler contract language. If you prefer to learn from examples, you can start with Tutorial 1 and come back here as a reference.
1. Contract Structure
Each .ct file can only define one contract. The basic skeleton is as follows:
Contract ContractName:
# Struct definitions (optional, can have multiple)
Struct StructName:
fieldName: type
# Public functions (at least one, serving as the spending entry point)
def functionName(param: type):
...
# Private helper functions (optional, prefixed with underscore)
def _helperFunction(param: type):
...Contract names, function names, and field names are identifiers following the same rules as Python: starting with a letter or underscore, followed by letters, digits, or underscores.
2. Data Types
Primitive Types
| Type | Description | Literal Example |
|---|---|---|
int,number | Integer (BVM big integer) | 0, 42, -100 |
string | Byte string | "hello", "world" |
hex | Hexadecimal byte array | 0x1234, 0xdeadbeef |
bool | Boolean value | 1 (true), 0 (false) |
address | Bitcoin P2PKH address | "1RainRzqJtJxHTngafpCejDLfYq2y4KBc" |
The address type only supports standard Base58 P2PKH addresses (starting with 1, 34 characters). hex and string are both byte sequences at the underlying level; the difference is only in how the literals are written.
Fixed-length hex Type
In struct fields, you can use hexN to declare fixed-byte-length hexadecimal fields, commonly used to describe fixed-format fields in Bitcoin transactions:
Struct TxInput:
txid: hex32 # 32-byte transaction ID
vout: hex4 # 4-byte output index
sequence: hex4 # 4-byte sequence numberArray Types
Struct fields support fixed-length arrays Type[N]:
Struct Transaction:
Inputs: TxInput[3] # 3 inputs
Outputs: TxOutput[3] # 3 outputsArray subscript access: tx.Inputs[0], where the subscript is usually an integer literal; if the subscript is an integer variable, it is generally used in combination with struct array fields to access the corresponding struct field.
vout = BinToNum(BVM.unlockingInput.Slice(32, 4)) # Get the position of code in the parent transaction output
vout_copy = vout.Clone()
# Get code_data
code_data = pretx.Outputs[vout_copy].LockingScript.SuffixData.Clone()uint64[] Array
Besides struct arrays, contracts also commonly use uint64[] (written as uint64[N]) to represent fixed-length 64-bit unsigned integer arrays. When not in use, the array field as a whole occupies one stack position; when needed, it is moved to the top via OP_ROLL, then split into individual elements via multiple OP_SPLIT operations.
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_taxKey points:
- The
Ninuint64[N]must be a compile-time-determinable fixed length; - Element access uses
arr[i], with indices recommended to traverse fixed boundaries withRange; uint64elements are processed as 8 bytes, suitable for integer sequences like amounts, counters, and indices.
If you need to express a set of count values in a struct, you can also declare directly:
Struct BatchData:
counts: uint64[4]Inline Anonymous Struct Types
For compound fields used temporarily, you can inline them directly without defining a separate struct:
utxoData: {txid: hex32, vout: hex4, sequence: hex4}
utxoData = Push(BVM.unlockingInput)
vout = BinToNum(utxoData.vout)3. Variables
Assignment
count = 10
result = Hash160(pubKey) # Bind function return value to a variableLocal variables (except arrays and inline anonymous struct types) can be directly assigned and used; struct fields and function parameters must declare types.
Contract Member Variables (self)
Contract member variables can be used directly; they are replaced with fixed constants in the bytecode at compile time and can be read multiple times in both public and private functions, not subject to ownership restrictions:
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. Operators
Arithmetic Operators
sum = a + b
diff = a - b
prod = a * b
quot = a / b # Integer divisionThere is no modulo operator; use the built-in function Mod(a, b).
Comparison Operators
a == b # Equal to
a != b # Not equal to
a < b # Less than
a > b # Greater than
a <= b # Less than or equal to
a >= b # Greater than or equal toComparison operators return integer 1 (true) or 0 (false), usable directly in if conditions or Return.
Logical Operators
The contract language has no and / or / not keywords; logical operations are all handled by built-in functions:
ok = And(condition1, condition2) # Logical AND
ok = Or(condition1, condition2) # Logical OR
ok = Not(condition) # Logical NOT5. Control Flow
Conditional Statements
if amount > threshold:
CheckSigVerify(sig, pubKey)
else:
Return (1 == 0) # Rejectif and else branches must appear in pairs; multiple branches require nested if:
if role == 1:
_handleBuyer(sig, pubKey)
else:
if role == 2:
_handleSeller(sig, pubKey)
else:
Return (1 == 0)Loop Statements
Loops use the for ... in Range(start, stop, step) form, with semantics similar to Python's range, but the parameter order is (start, stop_exclusive, step):
# Decrement from 2 to 0 (inclusive)
for i in Range(2, -1, -1):
data = Cat(items[i], data)
# Increment from 0 to 2 (inclusive)
for i in Range(0, 3, 1):
total = Add(total, values[i])The loop count must be determined at compile time; primarily used to iterate over fixed-size arrays or perform a fixed number of operations.
6. Functions
Public Functions (Spending Entry Points)
Functions not starting with _ are public functions; each public function corresponds to an independent contract "spending path." Public functions execute in declaration order:
Contract MultiPath:
# Path 1: owner normal spend
def spend(sig: hex, pubKey: hex):
...
# Path 2: refund after timeout
def refund(sig: hex, pubKey: hex):
...Private Helper Functions
Functions starting with _ can only be called internally within the contract, used to encapsulate repetitive logic:
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 Statement
Return (capitalized) pushes the expression result onto the execution stack and generates an "OP_RETURN":
Return (1 == 1) # Always pass
Return (1 == 0) # Always reject
Return CheckSig(sig, pubKey) # Signature verification result as return value
Return And(cond1, cond2) # Combined conditionLowercase return is used in private functions to return a value to the caller:
def _computeHash(data: hex):
result = Hash160(data)
return result7. Structs
Structs describe the byte layout of composite data, typically corresponding to the format of a segment of transaction data:
Struct Script:
SuffixData: string
PartialHash: string
Size: int
Struct Output:
Value: int
LockingScript: Script # Structs can be nested
Struct PreTX:
VLIO: string
Inputs: Input[3]
UnlockingScriptHash: string
Outputs: Output[3]Struct field access uses the . operator, supporting multi-level chained access:
scriptSize = pretx.Outputs[0].LockingScript.SizeNote: Field access consumes the ownership of that field. Accessing the same field twice requires
.Clone()before the first access. See Ownership System for details.
8. Destructuring Assignment
The {} syntax is used to receive results from functions that return multiple values, or to initialize structs:
# Receive two return values from Split
{header, body} = Split(rawData, 4)
# Receive multiple return values from a private function
{x, y} = _getCoords(encoded)
# Struct literal initialization (in field order)
point: Point = {10, 20}9. Comments
Use # for line comments:
# This is a full-line comment
count: int = 0 # This is an end-of-line commentMulti-line comments are not currently supported; longer comments need # on each line.
Next Steps
- How to Deploy and Call a Contract — Compilation output and on-chain calling process