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:
import std.p2pkh # Optional: import libraries before the contract
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.
Libraries and imports
A .ct file still defines exactly one Contract, but it can reuse helper code before that contract. There are two common import styles:
import std.p2pkh # Resolve std/p2pkh.ct under the standard-library root
import "./lib/math.ct" # Resolve relative to the current fileA library is declared with Library <name>:. It can define Structs and helper functions, but it must not define contract-only entries such as main:
Library std.p2pkh:
def verifyP2PKH(signature: hex, pubKey: hex, pubKeyHash: hex):
EqualVerify(Hash160(pubKey.Clone()), pubKeyHash)
result = CheckSig(signature, pubKey)
return resultImported library members are available to the host contract at compile time. Library functions are treated like private helpers: they can be called from the contract, but they are not exported as public ABI spending entries even when their names do not start with _.
Pass instance data explicitly from the host contract into the library call. For example, std.p2pkh receives pubKeyHash as an argument, and the host contract can pass self.pubKeyHash during the call.
import std.p2pkh
Contract Wallet:
def main(signature: hex, pubKey: hex):
ok = verifyP2PKH(signature, pubKey, self.pubKeyHash)
importdetects circular imports, and duplicate imports of the same library are expanded only once. Use your compiler configuration to set the standard-library search path when needed.
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 numberFixed-length hexN types are often used together with inline anonymous struct types when you need a temporary view over an exact byte layout.
Array Types
Struct fields, function parameters, and self members support fixed-length arrays Type[N]. Common forms are shown below.
Struct fields can declare the field type as Type[N]:
Struct Transaction:
Inputs: TxInput[3] # 3 inputs
Outputs: TxOutput[3] # 3 outputsFunction parameters can also be fixed-length arrays:
def main(sigs: hex[3], pubKeys: hex[3], masks: bool[3]):
...self members are instance data substituted during deployment, and can also be read through fixed-array subscripts:
for i in Range(2, -1, -1):
if masks[i] == 1:
SetMain(total)
if And(Equal(Hash160(pubKeys[i].Clone()), self.addr[i]), CheckSig(sigs[i], pubKeys[i])):
total = total + 1
SetAlt(total)
else:
total = total + 0
SetAlt(total)
else:
Delete(pubKeys[i])
Delete(sigs[i])Array subscript access includes tx.Inputs[0], pubKeys[i], and self.pubKeyHashes[0]. The index may be an integer literal or a variable produced by a fixed Range loop. If the index is an integer variable, it is usually used together with a struct array field 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. They are exported in the ABI and execute serially in declaration order:
Contract MultiPath:
# Step 1: verify previous transaction/state
def loadPreviousState(pretx: PreTX):
...
# Step 2: verify current transaction
def verifyCurrent(ctx: CurrentTX):
...This is different from SDKs where the caller selects one public method. All public functions form one continuous locking-script flow. For mutually exclusive paths, prefer one public entry with a path argument and dispatch internally:
def main(sig: hex, pubKey: hex, path: int):
if path == 0:
_spend(sig, pubKey)
else:
_refund(sig, pubKey)This keeps the unlocking interface clear: the caller supplies one path selector plus the parameters needed by that path.
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 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. Common Contract Patterns
Most contracts fall into a few practical families: signature locks, branch locks, state-continuation contracts, and transaction-structure verification contracts. When writing a new contract, first decide which family it belongs to, then start from the closest skeleton.
Minimal Signature Lock: Prefer Libraries
For a simple P2PKH wallet, avoid rewriting the script template. Import the standard helper instead:
import std.p2pkh
Contract MyWallet:
def main(signature: hex, pubKey: hex):
ok = verifyP2PKH(signature, pubKey, self.pubKeyHash)The host contract passes self.pubKeyHash into the library function; deployment replaces that placeholder with the real 20-byte Hash160. The contract file keeps the business entry small while the reusable standard check lives in the library.
Multi-Path Contracts: One Entry Plus path
Because public functions run serially, mutually exclusive paths should not be represented as multiple public methods. Use a main function that receives path, executes the selected branch, and clears branch-unused parameters with Delete.
Contract AtomicSwap:
def main(x: hex, sig: hex, path: number):
if path == 1:
EqualVerify(Sha256(x), self.hashX)
CheckSigVerify(sig, self.receiver)
else:
Delete(x)
NumEqualVerify(GreaterOrEqual(BVM.locktime, self.timeout), 1)
CheckSigVerify(sig, self.sender)This structure fits timeout refunds, auction closing, swap cancellation, and similar flows. As branches grow, keep main as the dispatcher and move each path into a private function.
State Continuation: Verify Parent and Current Outputs
State-continuation contracts spend an old state UTXO and require the new transaction to create a valid next state. A common structure is:
Struct Script:
SuffixData: string
PartialHash: string
Size: number
Struct Output:
Value: number
LockingScript: Script
Struct Input:
Data: {txid: hex32, vout: hex4, sequence: hex4}
Struct PreTX:
VLIO: string
Inputs: Input[3]
UnlockingScriptHash: string
Outputs: Output[3]
Struct CurrentTX:
Outputs: Output[3]Parent transaction checks usually do three things:
- Read the
txidandvoutreferenced by the current input fromBVM.unlockingInput; - Rebuild the parent transaction hash from
pretxand compare it with the inputtxid; - Read old state, old value, and old script
PartialHash/Sizefrompretx.Outputs[vout].
Current transaction checks usually do two things:
- Ensure an output continues the same contract code segment, for example unchanged
PartialHashandSize; - Concatenate every committed output, compute
Sha256(outputs_data), and compare it withBVM.outputsHash.
outputs_data = Push(0)
SetAlt(outputs_data)
for i in Range(2, -1, -1):
size = ctx.Outputs[i].LockingScript.Size.Clone()
if size != 0:
temp = PartialHash(ctx.Outputs[i].LockingScript.SuffixData, ctx.Outputs[i].LockingScript.PartialHash, ctx.Outputs[i].LockingScript.Size)
temp = Cat(ctx.Outputs[i].Value, temp)
SetMain(outputs_data)
outputs_data = Cat(temp, outputs_data)
SetAlt(outputs_data)
else:
Delete(ctx.Outputs[i].LockingScript.Size)
Delete(ctx.Outputs[i].LockingScript.PartialHash)
Delete(ctx.Outputs[i].LockingScript.SuffixData)
Delete(ctx.Outputs[i].Value)
SetMain(outputs_data)
EqualVerify(Sha256(outputs_data), BVM.outputsHash)Oracles and External Messages: Verify, Then Parse
For oracle or external-message contracts, clone the message once for parsing, then pass the original message into signature verification.
msg_for_parse = msg.Clone()
schnorrVerify(msg, R, s, self.oraclePubKey, self.generator, self.modulus)
{asset_id, rest1} = Split(msg_for_parse, 32)
{timestamp_b, price_b} = Split(rest1, 8)
EqualVerify(asset_id, self.assetId)For unsigned little-endian integer fields, a common pattern is to append 0x00 before numeric conversion so the highest bit is not interpreted as a negative sign:
price_num = BinToNum(Cat(price_b, 0x00))Writing Checklist
- Decide the entry model first: use
mainfor a single path,main(path, ...)for mutually exclusive paths, and multiple public functions only for serial verification steps; - Describe every transaction fragment with
Structinstead of scattering magic offsets through the contract; - If a local variable or struct field is read twice, call
.Clone()before the first consuming use; - Clear parameters, fields, or structs unused by a branch with
Delete(...); - Keep loop bounds fixed; use
Rangefor batch signatures, batch outputs, and fixed-field parsing; - Use
SetAlt/SetMainfor intermediate values that must survive loops or branches; - When checking current outputs, finish with an aggregate
BVM.outputsHashcommitment; - When checking a parent transaction, rebuild
preTXIDand compare it withBVM.unlockingInput.Slice(0, 32).
10. 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.
Beginner Pitfalls
- Putting multiple contracts in one file: each
.ctfile can contain only oneContract. - Using multiple public functions for mutually exclusive paths: public functions run serially; use
main(path, ...)for branch dispatch. - Reusing a consumed variable: local variables are often consumed after being passed to built-ins; call
.Clone()before reuse. - Using runtime values as loop bounds:
Range()bounds must be known at compile time. - Serializing struct fields in a different order: transaction data must follow the order declared by the
Struct.
Quick Review
- A contract file consists of one
Contract, typed declarations, functions, and optionalStructs. - Public functions are spending entries; private functions are for internal decomposition.
self.Xis instance data and can be read repeatedly without normal local-variable ownership limits.Clonecopies reusable values, whileDeleteclears unused values.- Stateful contracts usually verify both the parent transaction and the current transaction outputs.
Next Steps
- How to Deploy and Call a Contract — Compilation output and on-chain calling process