A comprehensive guide to learning ChiaLisp for Chia blockchain development
In Chia, there are no “accounts” or “balances” like in Ethereum. Instead, the entire blockchain state is represented as a set of unspent coins. This is called the UTXO model (Unspent Transaction Output), similar to Bitcoin but with a critical difference: in Chia, every coin carries a program (a puzzle) that determines how it can be spent.
Traditional Account Model Chia Coin Model
======================== ================
Account: 0xABC... Coin #1: 0.5 XCH (puzzle A)
Balance: 3.5 XCH Coin #2: 1.0 XCH (puzzle A)
Coin #3: 2.0 XCH (puzzle B)
-------------------------
Total: 3.5 XCH
Every coin in Chia has exactly three properties:
+---------------------------------------------+
| COIN |
+---------------------------------------------+
| parent_coin_id : bytes32 (32 bytes) |
| puzzle_hash : bytes32 (32 bytes) |
| amount : uint64 (in mojos) |
+---------------------------------------------+
parent_coin_id - The coin ID of the coin that created this one. Every coin (except the genesis coin) was created by spending another coin.
puzzle_hash - The SHA-256 hash of the compiled ChiaLisp puzzle that locks this coin. This determines WHO can spend it and UNDER WHAT CONDITIONS.
amount - How many mojos this coin is worth (1 XCH = 1,000,000,000,000 mojos).
The coin’s unique identifier is computed deterministically:
coin_id = sha256(parent_coin_id + puzzle_hash + amount)
This means:
parent_coin_id (32 bytes)
|
v
+-------------+
| |
| SHA-256 | <--- puzzle_hash (32 bytes)
| |
+-------------+ <--- amount (8 bytes, big-endian)
|
v
coin_id (32 bytes)
You never modify a coin. Instead:
BEFORE SPEND AFTER SPEND
============ ===========
[Coin A: 5 XCH] --spend--> [Coin B: 3 XCH] (payment)
[Coin C: 2 XCH] (change)
Coin A is DESTROYED.
Coins B and C are CREATED.
This is exactly like physical cash: you cannot modify a $5 bill to become a $3 bill. You break it into a $3 and a $2. The original is gone.
The puzzle_hash in a coin references a ChiaLisp program. This program is the sole authority over what happens when the coin is spent. There is no external authority, no admin key, no override – only the puzzle’s logic matters.
"Who can spend this coin?" --> Determined by the puzzle
"Where can the funds go?" --> Determined by the puzzle
"Are there time restrictions?" --> Determined by the puzzle
"Does it need a signature?" --> Determined by the puzzle
A puzzle is a ChiaLisp program that is referenced by coins (via its hash). When someone wants to spend a coin, they must:
+------------------+
solution ---->| |
| PUZZLE |----> CONDITIONS
| (ChiaLisp pgm) | (list of opcodes)
| |
+------------------+
The puzzle is the JUDGE.
The solution is the EVIDENCE.
The conditions are the VERDICT.
Step 1: WRITE the puzzle in ChiaLisp (.clsp file)
(mod (password) ...)
Step 2: COMPILE the puzzle to CLVM (serialized bytecode)
$ run my_puzzle.clsp
(a (q 2 ... ) (c (q . ...) 1))
Step 3: HASH the compiled puzzle
$ opc -H '(a (q 2 ...) ...)'
puzzle_hash = 0x4bf5122f...
Step 4: CREATE a coin that references this puzzle_hash
The coin now exists on chain, locked by this puzzle.
Step 5: SPEND the coin by revealing the full puzzle + solution
Nodes verify: sha256(revealed_puzzle) == coin.puzzle_hash
Then run: (revealed_puzzle solution) --> conditions
On-chain, coins only store the puzzle_hash (32 bytes), not the full puzzle. This is important:
| Aspect | Puzzle | Solution |
|---|---|---|
| When set | When coin is created | When coin is spent |
| Who decides | The coin creator | The coin spender |
| On chain | Only the hash (until spent) | Revealed at spend time |
| Purpose | Define the rules | Provide the inputs |
| Analogy | A lock | A key |
; Puzzle: require the solution to be the number 42
(mod (answer)
(if (= answer 42)
() ; success - return empty conditions
(x) ; fail - raise exception
)
)
If you provide solution (42), the puzzle succeeds.
If you provide solution (99), the puzzle raises an exception and the spend fails.
When a puzzle runs successfully, it returns a list of conditions. Each condition is a list where the first element is an opcode (a number) and the remaining elements are arguments to that condition.
conditions = (
(OPCODE_1 arg1 arg2 ...)
(OPCODE_2 arg1 arg2 ...)
...
)
The Chia network reads these conditions and enforces them. If any condition cannot be satisfied, the entire spend (and possibly the entire transaction) fails.
Here is every important condition opcode:
Creates a new coin on the blockchain.
(51 puzzle_hash amount ...memos)
| Argument | Type | Description |
|---|---|---|
| puzzle_hash | bytes32 | The puzzle hash for the new coin |
| amount | uint64 | Amount in mojos |
| memos | list | Optional. Used for hints/recipient info |
What it does: This is how value moves in Chia. When you spend a coin, you create new coins with CREATE_COIN. The sum of created coin amounts (plus fees) must equal the sum of spent coin amounts in the transaction.
When to use it:
Common patterns:
; Send 1 XCH to someone (puzzle_hash is their address)
(51 0xTHEIR_PUZZLE_HASH 1000000000000)
; Create a change coin back to yourself
(51 0xMY_PUZZLE_HASH remaining_amount)
; Create a coin with memos (for wallet discovery)
(51 0xTHEIR_PUZZLE_HASH 1000000000000 (0xTHEIR_PUZZLE_HASH))
Security implications:
Requires a BLS aggregate signature from a specific public key over a specific message.
(49 public_key message)
| Argument | Type | Description |
|---|---|---|
| public_key | G1Point | 48-byte BLS public key |
| message | bytes | The message that must be signed |
What it does: Requires that the transaction’s aggregated BLS signature includes a signature from the given public key over the given message. “Unsafe” because the message is used as-is, without binding to any specific coin.
When to use it:
Security implications:
Requires a BLS aggregate signature bound to THIS specific coin.
(50 public_key message)
| Argument | Type | Description |
|---|---|---|
| public_key | G1Point | 48-byte BLS public key |
| message | bytes | The message that must be signed |
What it does: Like AGG_SIG_UNSAFE, but the actual message that must be signed is:
signed_data = message + coin_id + GENESIS_CHALLENGE
This binds the signature to this specific coin on this specific blockchain (mainnet vs testnet).
When to use it:
Common patterns:
; Require the owner's signature (standard pattern)
; PUBLIC_KEY is curried in, message is empty or specific
(50 PUBLIC_KEY ())
Security implications:
Verifies that this coin’s ID matches the given value.
(70 coin_id)
What it does: Asserts that the coin being spent has the given coin_id. If it does not match, the spend fails.
When to use it:
Common patterns:
; Verify we are the coin we think we are
(70 my_coin_id) ; my_coin_id comes from solution
Security implications:
Verifies this coin’s parent coin ID.
(71 parent_coin_id)
What it does: Asserts that the coin being spent was created by the coin with the given ID.
When to use it:
Security implications:
Verifies this coin’s puzzle hash.
(72 puzzle_hash)
What it does: Asserts that the coin being spent has the given puzzle hash.
When to use it:
Common patterns:
; Create a child coin with the same puzzle
(72 MY_PUZZLE_HASH) ; assert our own puzzle hash
(51 MY_PUZZLE_HASH amount) ; create child with same puzzle
Verifies this coin’s amount.
(73 amount)
What it does: Asserts that the coin being spent holds the given amount in mojos.
When to use it:
Security implications:
Example of the attack prevented:
Your coin: 100 mojos, puzzle creates coin of 100 mojos to recipient.
Attacker creates: 1000 mojos coin with SAME puzzle.
Attacker spends it: puzzle creates coin of 100 mojos to recipient.
900 mojos become fees --> stolen by attacker (as a farmer).
Fix: puzzle asserts (73 100), so it cannot run on the 1000-mojo coin.
The spend is only valid if at least N seconds have passed since this coin was created.
(80 seconds)
When to use it:
The spend is only valid after a specific UNIX timestamp.
(81 timestamp)
When to use it:
The spend is only valid if at least N blocks have been created since this coin was created.
(82 block_count)
When to use it:
The spend is only valid after a specific block height.
(83 block_height)
When to use it:
Reserves a minimum transaction fee.
(52 fee_amount)
What it does:
Ensures that at least fee_amount mojos are left over as fees in this transaction.
The fee is the difference between total input and total output amounts.
When to use it:
Common patterns:
; Require at least 1 million mojos in fees
(52 1000000)
Creates an announcement that other coins can assert.
(60 message)
What it does: Broadcasts a message from this coin. The announcement ID is:
announcement_id = sha256(coin_id + message)
Other coins in the same transaction can assert this announcement exists.
When to use it:
Asserts that a specific coin announcement exists in this transaction.
(61 announcement_id)
Where announcement_id = sha256(announcing_coin_id + message).
What it does: Fails the spend unless another coin in the same transaction created a matching announcement. This creates a dependency between coins.
When to use it:
Creates a puzzle announcement (identified by puzzle hash instead of coin ID).
(62 message)
The announcement ID is:
announcement_id = sha256(puzzle_hash + message)
When to use it:
Asserts that a specific puzzle announcement exists.
(63 announcement_id)
Where announcement_id = sha256(announcing_puzzle_hash + message).
A no-op condition. Does nothing but can carry data.
(1 ...data)
What it does: Nothing. The blockchain ignores it. But the data is visible on chain, so it can be used for metadata, comments, or tagging.
When to use it:
+--------+------------------------------+---------------------------------------+
| Opcode | Name | Arguments |
+--------+------------------------------+---------------------------------------+
| 1 | REMARK | ...data |
| 49 | AGG_SIG_UNSAFE | pubkey, message |
| 50 | AGG_SIG_ME | pubkey, message |
| 51 | CREATE_COIN | puzzle_hash, amount, ...memos |
| 52 | RESERVE_FEE | amount |
| 60 | CREATE_COIN_ANNOUNCEMENT | message |
| 61 | ASSERT_COIN_ANNOUNCEMENT | announcement_id |
| 62 | CREATE_PUZZLE_ANNOUNCEMENT | message |
| 63 | ASSERT_PUZZLE_ANNOUNCEMENT | announcement_id |
| 70 | ASSERT_MY_COIN_ID | coin_id |
| 71 | ASSERT_MY_PARENT_ID | parent_id |
| 72 | ASSERT_MY_PUZZLEHASH | puzzle_hash |
| 73 | ASSERT_MY_AMOUNT | amount |
| 80 | ASSERT_SECONDS_RELATIVE | seconds |
| 81 | ASSERT_SECONDS_ABSOLUTE | timestamp |
| 82 | ASSERT_HEIGHT_RELATIVE | block_count |
| 83 | ASSERT_HEIGHT_ABSOLUTE | block_height |
+--------+------------------------------+---------------------------------------+
In Chia, each coin is spent independently. Its puzzle runs, produces conditions, and those conditions are validated. But what if you need two coins to be spent together? What if coin A should only be spent if coin B is also spent in the same transaction?
This is where announcements come in.
Announcements are ephemeral messages that exist only within a single transaction. They are created by one coin and asserted by another.
Transaction
================================================
Coin A spends:
CONDITIONS:
(60 "hello") ; CREATE_COIN_ANNOUNCEMENT "hello"
Coin B spends:
CONDITIONS:
(61 <sha256(coinA_id + "hello")>) ; ASSERT_COIN_ANNOUNCEMENT
================================================
Result: Both spends succeed ONLY if they are in the same transaction.
If coin A is not present, coin B's assertion fails.
If coin B is not present, coin A still succeeds (it only creates, does not assert).
For coin announcements:
announcement_id = sha256(coin_id_of_announcer + message)
For puzzle announcements:
announcement_id = sha256(puzzle_hash_of_announcer + message)
The coin_id binding means that a coin announcement is unique to that specific coin. No other coin can fake it.
For truly atomic operations, both coins should announce AND assert:
Coin A: Coin B:
------ ------
CREATE_COIN_ANNOUNCEMENT "a2b" CREATE_COIN_ANNOUNCEMENT "b2a"
ASSERT_COIN_ANNOUNCEMENT ASSERT_COIN_ANNOUNCEMENT
sha256(B_id + "b2a") sha256(A_id + "a2b")
Neither can be spent without the other!
This is the foundation of:
Imagine a vault with two coins: a “key coin” and a “vault coin.” The vault can only be opened if the key coin is spent in the same transaction.
+-------------------+ +-------------------+
| KEY COIN | | VAULT COIN |
| | | |
| Creates announce: |--------->| Asserts announce: |
| "open_sesame" | | sha256(key_id + |
| | | "open_sesame") |
+-------------------+ +-------------------+
The vault coin CANNOT be spent unless the key coin
is also spent in the same transaction.
| Aspect | Coin Announcement (60/61) | Puzzle Announcement (62/63) |
|---|---|---|
| ID includes | coin_id of announcer | puzzle_hash of announcer |
| Uniqueness | Unique to specific coin | Any coin with that puzzle |
| Use case | Specific coin must participate | Any coin of a type can satisfy |
| Security | Stronger (tied to exact coin) | Weaker (any matching puzzle) |
Rule of thumb: Use coin announcements unless you have a specific reason to use puzzle announcements.
Chia uses BLS (Boneh-Lynn-Shacham) signatures on the BLS12-381 curve. The key property of BLS signatures is aggregation: multiple signatures from multiple keys can be combined into a single signature that can be verified in one step.
Traditional: BLS Aggregation:
sig1 = sign(sk1, msg1) sig1 = sign(sk1, msg1)
sig2 = sign(sk2, msg2) sig2 = sign(sk2, msg2)
sig3 = sign(sk3, msg3) agg_sig = aggregate(sig1, sig2, sig3)
Verify: check each one Verify: check agg_sig against
individually (3 checks) all (pk, msg) pairs (1 check)
A Chia transaction carries exactly ONE aggregated signature. All the AGG_SIG conditions from all coin spends in the transaction are verified against this single signature.
Private Key (sk) Public Key (pk)
32 bytes 48 bytes (G1 point)
KEEP SECRET Share freely
sk ---derive---> pk (one-way, cannot reverse)
sign(sk, message) --> signature (96 bytes, G2 point)
verify(pk, message, signature) --> true/false
When a puzzle outputs (50 pk msg):
1. The network computes the ACTUAL message to verify:
actual_msg = msg + coin_id + GENESIS_CHALLENGE
- coin_id: the ID of the coin being spent (binds to this coin)
- GENESIS_CHALLENGE: a constant per-network value
(different for mainnet and testnet, binds to this chain)
2. The aggregated signature in the transaction must include
a valid signature: sign(sk, actual_msg) where sk corresponds to pk.
3. All AGG_SIG conditions across all spends in the transaction are
collected, and the single aggregated signature is verified
against all of them at once.
AGG_SIG_UNSAFE (49):
--------------------
actual_msg = msg (just the raw message)
Problem: If you sign "approve" for coin A, that same signature
works for ANY coin that requires "approve" signed by your key.
An attacker can REPLAY your signature.
AGG_SIG_ME (50):
-----------------
actual_msg = msg + coin_id + GENESIS_CHALLENGE
Safe: The signature is bound to THIS coin on THIS network.
It cannot be replayed on any other coin, or on testnet vs mainnet.
The most common pattern (used by the standard wallet):
(mod (PUBLIC_KEY conditions)
; PUBLIC_KEY is curried in (baked into the puzzle)
; conditions come from the solution (delegated spending)
(list
(50 PUBLIC_KEY (sha256tree conditions)) ; sign the conditions
; ...then include the conditions
)
)
The owner signs the exact conditions they want, preventing anyone from modifying the transaction.
This is the most important security lesson in ChiaLisp. Consider this puzzle:
; INSECURE: password coin that lets solution specify outputs
(mod (password conditions)
(if (= (sha256 password) 0xABC123...)
conditions ; return whatever the spender wants
(x)
)
)
The problem: once someone sees the password in a spend on-chain, they can:
Fix: assert the coin ID in the solution.
(mod (password my_coin_id conditions)
(if (= (sha256 password) 0xABC123...)
(c (list 70 my_coin_id) conditions) ; ASSERT_MY_COIN_ID
(x)
)
)
Now the spend is bound to ONE specific coin and cannot be replayed.
A replay attack is when someone takes a valid spend and reuses it on a different coin. Defenses:
Defense Layers:
+-------------------------------------------------+
| AGG_SIG_ME: signature bound to this coin |
| +---------------------------------------------+ |
| | ASSERT_MY_COIN_ID: conditions bound to coin | |
| | +-----------------------------------------+ | |
| | | ASSERT_MY_AMOUNT: amount verified | | |
| | | +-------------------------------------+ | | |
| | | | CREATE_COIN: funds go where intended | | | |
| | | +-------------------------------------+ | | |
| | +-----------------------------------------+ | |
| +---------------------------------------------+ |
+-------------------------------------------------+
When you spend a coin of 10 XCH but only want to send 3 XCH, you must create a change coin for the remaining 7 XCH:
Spend: Coin (10 XCH)
Create: Payment Coin (3 XCH) --> to recipient
Create: Change Coin (7 XCH) --> back to yourself
If you forget the change coin:
10 XCH - 3 XCH = 7 XCH becomes FEES (lost!)
Always account for all value. The sum of CREATE_COIN amounts plus fees must equal the sum of spent coin amounts.
SUM(spent coins) = SUM(created coins) + fees
If your puzzle does not create coins for ALL the value,
the remainder becomes fees and is gone forever.
Every well-written coin should follow this pattern:
(mod (PUBLIC_KEY my_coin_id delegated_conditions)
; 1. Require a signature from the owner
; 2. Assert our own identity (prevent replay)
; 3. Return the signed conditions
(c (list 50 PUBLIC_KEY (sha256tree delegated_conditions)) ; AGG_SIG_ME
(c (list 70 my_coin_id) ; ASSERT_MY_COIN_ID
delegated_conditions ; user's conditions
))
)
See the examples/ directory for working ChiaLisp code:
| File | Description |
|---|---|
anyone_can_spend.clsp |
Simplest puzzle – anyone can spend it (insecure) |
password_coin.clsp |
Password-locked coin (still insecure – learn why) |
secure_password_coin.clsp |
Password coin done right with all protections |
signature_locked.clsp |
BLS signature locked coin (standard pattern) |
time_locked_coin.clsp |
Time-locked coin with relative and absolute variants |
announcement_pair.clsp |
Two coins that must be spent together |
Next chapter: Currying and Inner Puzzles