# Simplicity (draft)
> A typed, combinator-based, smart contract language for Bitcoin-like blockchains.
## Get Started
### Quickstart
# Simplicity Quickstart
Try a real smart contract transaction on [Liquid](../glossary.md#liquid) testnet with the `last_will.simf` contract.
You'll perform a [Liquid](../glossary.md#liquid) testnet transaction using the "last will" [covenant](../glossary.md#covenant). This allows an inheritor to claim funds after a delay if their benefactor goes silent, while the benefactor can postpone that indefinitely just by checking in.
## The contract
This [smart contract](../glossary.md#smart-contract), written in [SimplicityHL](../glossary.md#simplicityhl), is called `last_will.simf`. It implements a covenant with an inheritance-after-timeout pattern, using three keys and three spending paths:
* **Hot key**: the benefactor can "check in" at any time. Checking in re-creates the exact same contract at a new [UTXO](../glossary.md#utxo) (a [recursive covenant](../glossary.md#recursive-covenant)) and resets the inheritor's timelock. This is the key the benefactor is expected to use day-to-day.
* **Cold key**: the benefactor can also break out of the covenant entirely and move the funds anywhere, no matter what the timelock says. This key would typically be kept offline, since it's only needed in unusual circumstances. This quickstart doesn't exercise this path.
* **Inheritor key**: after a [timelock](../glossary.md#timelock) has elapsed, the inheritor can claim everything.
```mermaid
%%{init: {'flowchart': {'nodeSpacing': 150, 'rankSpacing': 100}}}%%
flowchart TD
F[Liquid testnet faucet] -- Funds --> C[Contract]
C -- "Hot key: check in (repeat any number of times)" --> C
C -- "Inheritor key: after timelock" --> H[Heir's wallet]
C -. "Cold key: any time" .-> O[Owner's own wallet]
```
The code (from the online SimplicityHL examples) looks like this:
```rust
fn checksig(pk: Pubkey, sig: Signature) {
let msg: u256 = jet::sig_all_hash();
jet::bip_0340_verify((pk, msg), sig);
}
fn enforce_relative_distance(min_distance: Distance) {
// Transaction version must be at least 2 for BIP68 relative locktime to apply.
assert!(jet::le_32(2, jet::version()));
// Fetch and parse the current input's own sequence number.
let actual_data: Either = unwrap(jet::parse_sequence(jet::current_sequence()));
let actual_distance: Distance = unwrap_left::(actual_data);
assert!(jet::le_16(min_distance, actual_distance));
}
// Enforce the covenant to repeat in the first output.
//
// Elements has explicit fee outputs, so enforce a fee output in the second output.
// Disallow further outputs.
fn recursive_covenant() {
assert!(jet::eq_32(jet::num_outputs(), 2));
let this_script_hash: u256 = jet::current_script_hash();
let output_script_hash: u256 = unwrap(jet::output_script_hash(0));
assert!(jet::eq_256(this_script_hash, output_script_hash));
assert!(unwrap(jet::output_is_fee(1)));
}
fn inherit_spend(inheritor_sig: Signature) {
let min_distance: Distance = param::MIN_DISTANCE_BLOCKS;
enforce_relative_distance(min_distance);
let inheritor_pk: Pubkey = param::INHERITOR_PUBLIC_KEY;
checksig(inheritor_pk, inheritor_sig);
}
fn cold_spend(cold_sig: Signature) {
let cold_pk: Pubkey = param::COLD_PUBLIC_KEY;
checksig(cold_pk, cold_sig);
}
fn refresh_spend(hot_sig: Signature) {
let hot_pk: Pubkey = param::HOT_PUBLIC_KEY;
checksig(hot_pk, hot_sig);
recursive_covenant();
}
enum Action {
Inherit(Signature),
ColdSpend(Signature),
HotSpend(Signature),
}
fn main() {
match witness::ACTION {
Action::Inherit(sig: Signature) => inherit_spend(sig),
Action::ColdSpend(sig: Signature) => cold_spend(sig),
Action::HotSpend(sig: Signature) => refresh_spend(sig),
}
}
```
You'll play the roles both of the benefactor and the inheritor, using two different [private keys](../glossary.md#private-key). In a real deployment, these keys would belong to different people. The benefactor would also have a "cold key", most likely kept offline.
Please choose your preferred language environment immediately below. You can also run any version [online, with no download](https://github.com/Blockstream/simplicity-codespace/).
=== "Rust"
## Rust quickstart
Before beginning, please make sure you have Rust installed.
## Demo walkthrough
!!! tip "Need help?"
If you get stuck at any point in this tutorial, ask in the [Simplicity Community](https://community.simplicity-lang.org/) forum. Questions posted there are public, so the answer can help later readers too.
### 1. Clone the walkthrough git repository
```bash
git clone https://github.com/BlockstreamResearch/simplicity-demo
cd simplicity-demo
```
### 2. Create a random seed for the demo's keypairs
Run this command to create a random seed value. (This will be used to derive keypairs used in the demo.)
```bash
openssl rand -hex 32
```
Create an `.env.demo` file at the top level of the `simplicity-demo` project, with a single line `SEED_HEX=` followed by your random seed value from the previous command.
Every `last-will` command below derives its keys from this one seed, so you don't need to manage three separate seeds to play all three roles yourself. In a real deployment, these would be three unrelated seeds held on different devices.
### 3. Compile the last-will contract
```bash
cargo run last-will compile-to-testnet-address -v
```
This derives all three keypairs from the seed, substitutes their public keys (and the default `--min-distance-blocks 3`) into `last_will.simf`, and compiles it. The output will look something like this:
```
# Deriving keypairs from seed.
# Compiling SimplicityHL program source_simf/last_will.simf.
SimplicityHL source code:
fn checksig(pk: Pubkey, sig: Signature) {
...
}
...
Parameter arguments (compile-time):
mod param {
const COLD_PUBLIC_KEY: u256 = 0x0804...;
const HOT_PUBLIC_KEY: u256 = 0x4f35...;
const INHERITOR_PUBLIC_KEY: u256 = 0x55f1...;
const MIN_DISTANCE_BLOCKS: u16 = 3;
}
# Deriving Liquid Testnet address.
---> Inheritor public key: 55f1cd1f0ebdd80e86080ab56c02fb3f65c540f880f4e5e48bc1800e74c13606
---> Cold public key: 08040427cb5728a184a886897f0faed50353919e24b0571de75c45ef799821be
---> Hot public key: 4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa
Contract's Liquid Testnet address: tex1p6df7ur00f9hc3k3y2g9ls6tl963sg59vm3pe9urytupxkhlpyrestuh6nm
```
(Without `-v`, you'll just see the address on the last line.) `MIN_DISTANCE_BLOCKS` defaults to 3 purely so this quickstart finishes in a few minutes, as Liquid Network blocks are created once per minute.
### 4. Fund the contract on Liquid testnet
```bash
cargo run last-will fund-from-faucet --address tex1p6df7ur00f9hc3k3y2g9ls6tl963sg59vm3pe9urytupxkhlpyrestuh6nm
```
**(Substitute the address from your own Step 3 output.)** This funds the contract with 100000 sats of tLBTC, and prints the funding transaction's ID; you'll need it for the next step. Wait for it to confirm (check the Explorer) before continuing, since a relative timelock's clock starts at the confirming block, not at broadcast time.
### 5. Check in with the hot key
This is the transaction the benefactor is expected to send periodically: it spends the current UTXO straight back to the *same contract address*, minus a fee, signed with the hot key. Producing this transaction is what resets the inheritor's timelock.
```bash
cargo run last-will prove-alive --utxo :0 --fee-sats 100 --broadcast
```
Replace `` with the transaction ID from Step 4. This derives all three keypairs again (needed to recompile the identical contract and re-derive its address), builds a transaction paying the contract's balance minus the fee back to itself, signs it with the hot key, builds the `Action::HotSpend` [witness](../glossary.md#witness), and (because `--broadcast` is set) submits it and prints the resulting txid.
??? "What's happening here?"
Leave off `--broadcast` and the command prints the finalized raw transaction hex instead of submitting it.
Wait for this transaction to confirm too, and note the block height it confirms at (visible on the Explorer): call it `HOT_CONFIRM_HEIGHT`. The inheritor's timelock is satisfied starting at block `HOT_CONFIRM_HEIGHT + 3` (or whatever `--min-distance-blocks` you used).
### 6. Try to inherit early (should fail)
With the benefactor apparently still active, the inheritor shouldn't be able to claim anything yet:
```bash
cargo run last-will spend-as-inheritor \
--utxo :0 \
--to-address tex1q9hgs7pj8etd92rw5qz3dymvujffxzylmj6a28h \
--fee-sats 100 \
--broadcast
```
Replace `` with the txid from Step 5. This derives the keys, builds a transaction spending the hot-spend UTXO to the given destination, declares a relative-locktime distance of `--min-distance-blocks` (3 by default) on the input, signs with the inheritor's key, and builds the `Action::Inherit` witness, all of which succeeds locally, since the contract's own check only compares against the *declared* distance. The broadcast, though, should fail with something like `non-BIP68-final`: the node independently checks how many blocks have *actually* passed since `` confirmed, and it's still close to zero.
This is the same split explained on the [Timelocks](../documentation/timelocks.md#timelock-enforcement-mechanisms) page: Simplicity checks "does this transaction's declared `sequence` satisfy my rule?"; blockchain consensus separately checks "has enough real time actually passed to accept this `sequence`?" Both conditions must be satisfied.
### 7. Wait, then inherit for real
Wait until the chain tip reaches `HOT_CONFIRM_HEIGHT + 3` (watch the Explorer, or poll it the way the [bash/CLI tab](#__tabbed_1_2) above does). At a block a minute on Liquid testnet, this should take about three minutes.
Then run the *identical* command from Step 6 again:
```bash
cargo run last-will spend-as-inheritor \
--utxo :0 \
--to-address tex1q9hgs7pj8etd92rw5qz3dymvujffxzylmj6a28h \
--fee-sats 100 \
--broadcast
```
This time it should succeed, printing a txid. You can view the completed transaction on the Explorer.
### Congratulations
You've walked a single contract through all three roles of a realistic covenant: funding it, proving activity to keep it alive, watching the network itself enforce a timelock the contract applied, and finally exercising the fallback path once that timelock had elapsed.
??? "See more technical details"
All four `last-will` subcommands support the `-v` option for verbose output, including the full compiled SimplicityHL source and parameter/witness values.
#### Next steps
* Once you're building something real rather than following a tutorial, [Simplex](https://github.com/BlockstreamResearch/smplx) handles project scaffolding, dependencies, and test suites for larger SimplicityHL projects.
* Read more about how relative and absolute timelocks work, and why they can only enforce *minimum* times, in [Timelocks](../documentation/timelocks.md).
* Read more about state and recursive covenants in [Covenants & State Management](../documentation/state.md).
* Try the same story in the [bash/CLI](#__tabbed_1_2) tab above.
* See more example contracts demonstrating other SimplicityHL language features.
=== "bash/CLI"
## bash/CLI quickstart
Before beginning, please make sure you have installed the toolchain applications (`simc` and `hal-simplicity`). You'll also need `curl` and `jq`.
??? note "Want to skip typing individual commands?"
A complete script that runs every step below automatically is available at last-will-demo.sh. Download it and run `bash last-will-demo.sh`. The walkthrough below explains what it's doing, step by step.
## Demo walkthrough
!!! tip "Need help?"
If you get stuck at any point in this tutorial, ask in the [Simplicity Community](https://community.simplicity-lang.org/) forum. Questions posted there are public, so the answer can help later readers too.
### 1. Save the contract
Save the contract above as `last_will.simf`.
### 2. Set up parameters
```bash
INTERNAL_KEY="50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
PRIVKEY_INHERITOR="0000000000000000000000000000000000000000000000000000000000000001"
PRIVKEY_COLD="0000000000000000000000000000000000000000000000000000000000000002"
PRIVKEY_HOT="0000000000000000000000000000000000000000000000000000000000000003"
INHERITOR_PUBLIC_KEY="0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
COLD_PUBLIC_KEY="0xc6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5"
HOT_PUBLIC_KEY="0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9"
MIN_DISTANCE_BLOCKS=3
DESTINATION_ADDRESS=tex1q9hgs7pj8etd92rw5qz3dymvujffxzylmj6a28h
```
`INTERNAL_KEY` is the standard BIP-0341 unspendable [internal key](../glossary.md#internal-key), and the private keys are intentionally the small integers 1, 2, and 3. In a real contract these would be long random numbers, generated and held separately by each party. `DESTINATION_ADDRESS` is where the inheritor's claim will send funds; it defaults to a Liquid testnet faucet return address so nothing is wasted.
`MIN_DISTANCE_BLOCKS` is set to 3 purely so this quickstart finishes in a few minutes. [Liquid testnet blocks land once a minute](../documentation/timelocks.md#timelock-measurement-units), so you'll only have to wait three minutes in order to inherit the contract's funds. A longer, more realistic period can be achieved by raising this number, up to a point. `Distance` is capped at 65535, which at one block a minute is only about 45 days. A real deployment wanting something like a 180-day check-in period would need to use [a different timelock enforcement method](../documentation/timelocks.md#relative-timelock-in-simplicityhl) instead.
Now write out the `.args` file substituting these values into the contract's parameters:
```bash
cat > last_will.args << EOF
{
"INHERITOR_PUBLIC_KEY": "$INHERITOR_PUBLIC_KEY",
"COLD_PUBLIC_KEY": "$COLD_PUBLIC_KEY",
"HOT_PUBLIC_KEY": "$HOT_PUBLIC_KEY",
"MIN_DISTANCE_BLOCKS": "$MIN_DISTANCE_BLOCKS"
}
EOF
```
### 3. Compile and fund the contract
Check the result of compiling the contract with the parameters from the prior step:
```bash
simc -Z enums last_will.simf -a last_will.args
```
(This contract uses SimplicityHL's `enum` feature, which is still experimental. Compiling it requires passing `-Z enums` to `simc`.)
Now derive the contract's address and compute its [CMR](../glossary.md#cmr), which you'll need in later steps to attach the program to a [PSET](../glossary.md#pset):
```bash
COMPILED_PROGRAM=$(simc -Z enums last_will.simf -a last_will.args --json | jq -r .program)
CMR=$(hal-simplicity simplicity info "$COMPILED_PROGRAM" | jq -r .cmr)
CONTRACT_ADDRESS=$(hal-simplicity simplicity info "$COMPILED_PROGRAM" | jq -r .liquid_testnet_address_unconf)
```
Fund it from the Liquid testnet faucet, which provides free tLBTC assets for testing and experimentation:
```bash
FAUCET_TXID=$(curl "https://liquidtestnet.com/api/faucet?address=$CONTRACT_ADDRESS&action=lbtc" 2>/dev/null | jq -r .txid)
```
Wait for this transaction to confirm before continuing. You can check the Explorer, or `curl https://blockstream.info/liquidtestnet/api/tx/$FAUCET_TXID/status` until it reports `"confirmed": true`. This matters here specifically because a relative timelock's clock starts at the confirming block, not at broadcast time, so the steps below need this input to have actually landed in a block.
### 4. Check in with the hot key
This is the transaction the benefactor is expected to send periodically: it spends the current UTXO straight back to the *same contract address*, minus a [fee](../glossary.md#fee), signed with the hot key. Producing this transaction is what resets the inheritor's timelock. The covenant doesn't track a countdown anywhere; it just requires this specific action to keep happening, repeatedly creating a fresh copy of the same covenant.
First, fetch the details of the UTXO you're about to spend:
```bash
curl https://liquid.network/liquidtestnet/api/tx/$FAUCET_TXID > input-tx.json
HEX=$(jq -r '.vout[0].scriptpubkey' < input-tx.json)
ASSET=$(jq -r '.vout[0].asset' < input-tx.json)
VALUE="0.00"$(jq -r '.vout[0].value' < input-tx.json)
```
(FIXME: This method of converting satoshis to Bitcoin is not correct in general if the number of satoshis isn't exactly six digits long.)
Build a [PSET](../glossary.md#pset) with two [outputs](../glossary.md#output): the refreshed contract, and an explicit fee. (This 2-output shape is exactly what `recursive_covenant()` checks for above; the contract will reject anything else.)
```bash
PSET=$(hal-simplicity simplicity pset create \
'[ { "txid": "'"$FAUCET_TXID"'", "vout": 0 } ]' \
'[ { "'"$CONTRACT_ADDRESS"'": 0.00099900 }, { "fee": 0.00000100 } ]' \
| jq -r .pset)
PSET=$(hal-simplicity simplicity pset update-input "$PSET" 0 -i "$HEX:$ASSET:$VALUE" -c "$CMR" -p "$INTERNAL_KEY" | jq -r .pset)
```
Sign it with the hot key, and build the witness. `witness::ACTION` needs to carry the `Action::HotSpend` enum variant along with the signature:
```bash
HOT_SIGNATURE=$(hal-simplicity simplicity sighash "$PSET" 0 "$CMR" -x "$PRIVKEY_HOT" | jq -r .signature)
cat > last_will.wit << EOF
{
"ACTION": "Action::HotSpend(0x$HOT_SIGNATURE)"
}
EOF
```
Compile with the witness attached, finalize, extract, and broadcast:
```bash
PROGRAM=$(simc -Z enums last_will.simf -a last_will.args -w last_will.wit --json | jq -r .program)
WITNESS=$(simc -Z enums last_will.simf -a last_will.args -w last_will.wit --json | jq -r .witness)
PSET=$(hal-simplicity simplicity pset finalize "$PSET" 0 "$PROGRAM" "$WITNESS" | jq -r .pset)
RAW_TX=$(hal-simplicity simplicity pset extract "$PSET" | jq -r)
HOT_TXID=$(curl -X POST "https://blockstream.info/liquidtestnet/api/tx" -d "$RAW_TX" 2>/dev/null)
```
Wait for `HOT_TXID` to confirm before moving on: you'll need the [height](../glossary.md#height) it confirms at in a moment. Once the Explorer shows it as confirmed, fetch that height:
```bash
HOT_CONFIRM_HEIGHT=$(curl -sSL https://blockstream.info/liquidtestnet/api/tx/$HOT_TXID/status | jq -r .block_height)
```
### 5. Try to inherit early (should fail)
With the benefactor apparently still active, the inheritor shouldn't be able to claim anything yet. Let's confirm the contract actually enforces that, not just trust it.
Fetch the hot-spend output's details, the same way as before:
```bash
curl https://liquid.network/liquidtestnet/api/tx/$HOT_TXID > input-tx.json
HEX=$(jq -r '.vout[0].scriptpubkey' < input-tx.json)
ASSET=$(jq -r '.vout[0].asset' < input-tx.json)
VALUE="0.00"$(jq -r '.vout[0].value' < input-tx.json)
```
This time, build the PSET with a [`sequence`](../documentation/timelocks.md#creating-appropriate-transactions) value declaring a relative distance of `MIN_DISTANCE_BLOCKS`:
```bash
PSET=$(hal-simplicity simplicity pset create \
'[ { "txid": "'"$HOT_TXID"'", "vout": 0, "sequence": '"$MIN_DISTANCE_BLOCKS"' } ]' \
'[ { "'"$DESTINATION_ADDRESS"'": 0.00099800 }, { "fee": 0.00000100 } ]' \
| jq -r .pset)
PSET=$(hal-simplicity simplicity pset update-input "$PSET" 0 -i "$HEX:$ASSET:$VALUE" -c "$CMR" -p "$INTERNAL_KEY" | jq -r .pset)
```
Sign with the inheritor's key this time, and build an `Action::Inherit` witness:
```bash
INHERITOR_SIGNATURE=$(hal-simplicity simplicity sighash "$PSET" 0 "$CMR" -x "$PRIVKEY_INHERITOR" | jq -r .signature)
cat > last_will.wit << EOF
{
"ACTION": "Action::Inherit(0x$INHERITOR_SIGNATURE)"
}
EOF
```
```bash
PROGRAM=$(simc -Z enums last_will.simf -a last_will.args -w last_will.wit --json | jq -r .program)
WITNESS=$(simc -Z enums last_will.simf -a last_will.args -w last_will.wit --json | jq -r .witness)
PSET=$(hal-simplicity simplicity pset finalize "$PSET" 0 "$PROGRAM" "$WITNESS" | jq -r .pset)
INHERIT_RAW_TX=$(hal-simplicity simplicity pset extract "$PSET" | jq -r)
```
Note that `finalize` and `extract` both **succeed**. That's expected: the contract's own check only compares against the `sequence` value you just declared, and 3 satisfies "at least 3". Now try broadcasting it anyway:
```bash
curl -X POST "https://blockstream.info/liquidtestnet/api/tx" -d "$INHERIT_RAW_TX"
```
This should be rejected with something like `non-BIP68-final`. The contract approved the transaction; the [node](../glossary.md#node) didn't, because it independently checks how many blocks have *actually* passed since `HOT_TXID` confirmed, and it's still close to zero. This is the same split explained on the [Timelocks](../documentation/timelocks.md#timelock-enforcement-mechanisms) page: Simplicity checks "does this transaction's declared `sequence` satisfy my rule?"; blockchain consensus separately checks "has enough real time actually passed to accept this `sequence`?" Both conditions must be satisfied.
Hold onto `INHERIT_RAW_TX`: you'll resend the exact same bytes in Step 7, unchanged.
### 6. Wait
```bash
TARGET_HEIGHT=$((HOT_CONFIRM_HEIGHT + MIN_DISTANCE_BLOCKS))
echo "Waiting for block $TARGET_HEIGHT..."
```
Poll the current chain tip until it reaches `TARGET_HEIGHT`:
```bash
curl -sSL https://blockstream.info/liquidtestnet/api/blocks/tip/height
```
At a block a minute, this should take about three minutes. Watch the Explorer if you'd rather not poll from the command line.
### 7. Inherit for real
Once the tip has reached `TARGET_HEIGHT`, resend the *identical* transaction from Step 5:
```bash
curl -X POST "https://blockstream.info/liquidtestnet/api/tx" -d "$INHERIT_RAW_TX"
```
This time it should be accepted, and you'll get back a txid. You can view the completed transaction on the Explorer.
### Congratulations
You've walked a single contract through all three roles of a realistic covenant: funding it, proving activity to keep it alive, watching the network itself enforce a timelock the contract applied, and finally exercising the fallback path once that timelock had elapsed.
#### Next steps
* Once you're building something real rather than following a tutorial, [Simplex](https://github.com/BlockstreamResearch/smplx) handles project scaffolding, dependencies, and test suites for larger SimplicityHL projects.
* Read more about how relative and absolute timelocks work, and why they can only enforce *minimum* times, in [Timelocks](../documentation/timelocks.md).
* Read more about state and recursive covenants in [Covenants & State Management](../documentation/state.md).
* Try the same story in the [Rust](#__tabbed_1_1) tab above.
* See more example contracts demonstrating other SimplicityHL language features.
### Simplicity SDK (Simplex)
# Simplex
Simplex is a development framework and orchestration tool for SimplicityHL smart contracts. It assumes a Rust development environment, and helps you create applications and tools in Rust to interact with the underlying contract.
Simplex facilitates creating and running realistic smart contract integration tests, providing a straightforward way to define and run a suite of such tests in Rust. It automatically runs tests against a local instance of `elementsd`, creating a customizable [Liquid Network](../glossary.md#liquid)-like environment for test transactions. Thus, tests can be run without broadcasting transactions publicly on Liquid testnet.
A Simplex project's `Simplex.toml` file (automatically created by `simplex init`, as described below) configures the details of this test environment.
## Installing Simplex
You can get Simplex from [https://github.com/BlockstreamResearch/smplx](https://github.com/BlockstreamResearch/smplx), or use the auto-install script:
```bash
curl -L https://smplx.simplicity-lang.org | bash
simplexup
```
When Simplex is installed with `simplexup`, you can rerun `simplexup` at any time to upgrade to the most recent release.
## Using Simplex
### Creating a project
Run `simplex init` at the top-level directory of the project. This is ordinarily the same as the top-level directory of the associated Rust project (where the `Cargo.toml` file is found).
This step will add a complete starter project in place: `Cargo.toml` (already depending on `smplx-std`), `Simplex.toml`, `src/lib.rs` to let Rust code reference generated [artifacts](../glossary.md#artifacts), and starter contracts and tests.
Add or replace SimplicityHL source files in `simf/` as you go so Simplex can find them.
### Building artifacts
Run `simplex build` at the top-level directory of the project.
This process analyzes the SimplicityHL source code in `simf/` and creates corresponding Rust library files in `src/artifacts`, suitable for including from other Rust code. These library files define functions to build [witnesses](../glossary.md#witness) and blockchain [transactions](../glossary.md#transaction) to drive the individual SimplicityHL contract(s) within the Simplex project.
Whenever the SimplicityHL source code changes, re-run `simplex build` to regenerate the artifacts and keep them in sync with the contract's expectations.
???+ "SimplicityHL package dependencies"
If your contract's `.simf` import a separate SimplicityHL package (via `use` of an external crate, not just a local module), list it under `[dependencies]` in `Simplex.toml` and run `simplex install` to fetch it before building.
### Creating tests
Create `.rs` files in `tests/` containing your integration tests. Each such file defines a series of named integration tests as Rust functions, each with a prototype like
```rust
#[simplex::test]
fn test_name(context: simplex::TestContext) -> anyhow::Result<()> {
// Test code goes here.
Ok(())
}
```
### Running tests
Run `simplex test` to execute your complete integration test suite against a local `elementsd`. Adding `-v` will display verbose output from the tests.
Pass a name to run just one file's tests.
### Persistent regtest instance
`simplex test` normally spins up and tears down its own `elementsd`/Electrs pair for each run. Run `simplex regtest` in a separate terminal to instead start a standalone, long-lived node pair, then point `simplex test` at it via the `[test.rpc]`/`[test.esplora]` entries in `Simplex.toml`. This is useful when you want to inspect chain state between test runs instead of starting from scratch every time.
## Examples
[`BlockstreamResearch/smplx/examples`](https://github.com/BlockstreamResearch/smplx/tree/master/examples) contains sample Simplex projects demonstrating how to create integration tests.
The current examples are
* `examples/basic` (demonstrates a pay-to-public-key contract, called `p2pk.simf`)
Each has a `README.md` file describing the project and how to invoke its Simplex test suite.
You can also experiment with Simplex online in your browser using the [Simplicity Codespace](https://github.com/codespaces/new/blockstream/simplicity-codespace).
## Learn
### Execution Model
# On-chain Simplicity execution model
Simplicity is a special-purpose language. It works in the context of the Bitcoin transaction model on Bitcoin-like blockchains. This is sometimes also called the "[UTXO](../glossary.md#utxo) model". If you're already familiar with Bitcoin Script and the role it plays in the logic of Bitcoin transactions, you can think of Simplicity as a more expressive and more analyzable alternative to Bitcoin Script, useful for writing more complex conditions (such as recursive [covenants](../glossary.md#covenant) that can propagate conditions across multiple subsequent transactions).
This document will describe the context in which Simplicity programs run, and what they can and can't do as a result.
## The Simplicity and SimplicityHL environment
Developers generally write SimplicityHL, a higher-level language with a Rust-like syntax, which compiles to Simplicity. The SimplicityHL compiler translates SimplicityHL into Simplicity to run on Simplicity's abstract Bit Machine.
Simplicity programs are attached to UTXOs via [Taproot](../glossary.md#taproot) and define their spending conditions. However, the Simplicity program is only disclosed when redeeming (claiming) [assets](../glossary.md#asset). As part of the blockchain consensus process, all blockchain [nodes](../glossary.md#node) can confirm that it matches the commitment attached to the UTXO, and can run the program to confirm that it approves the proposed transaction.
## What Simplicity programs are used for
The basic task of every Simplicity program is to *consider a proposed blockchain transaction* and determine whether to *approve or disapprove* that transaction. More complex financial logic can be built out of one or more Simplicity programs working together to manage assets and their disposition across multiple related transactions. Together, the logic and rules governing a set of related blockchain transactions can be called a [smart contract](../glossary.md#smart-contract).
Designing a smart contract with Simplicity thus includes representing its logic as a series of on-chain transactions, and describing the rules that govern exactly when each transaction is permitted to occur.
## Where and how Simplicity programs run
Like Bitcoin Script scripts, Simplicity programs are *attached to [UTXO](../glossary.md#utxo)s* and define spending conditions for the UTXOs to which they are attached. A Simplicity program can, however, have more complex logic and functionality compared to a Bitcoin Script script.
The Simplicity program does not initiate or originate the transaction and does not decide anything about what the transaction should be (for instance, it does not calculate or choose destination addresses, although it can *constrain* them by rejecting transactions that specify inappropriate destinations).
In Bitcoin and related systems, anyone can propose any transaction at any time; the spending conditions associated with assets, such as those contained in the logic of a Simplicity program, form part of the rules that determine whether or not proposed transactions are valid and hence whether those transactions could eventually be recorded in a block and become part of the blockchain.
Whenever a [node](../glossary.md#node) examines a transaction involving UTXO controlled by a Simplicity program, the node will run that program to confirm that the program agrees to allow the transaction.
The information available to the program to use in making that decision consists of
* whatever details are hard-coded within the program (for example, trusted public key values),
* details of all the [input](../glossary.md#input)s and [output](../glossary.md#output)s of the proposed transaction, and
* [witness](../glossary.md#witness) data supplied by the creator of the transaction as input to the program.
Note that the program can directly check its own cryptographic identity by examining the input UTXO to which it was attached.
The [witness](../glossary.md#witness) supplied as part of the transaction by its creator represents inputs meant to provide additional context for the transaction. The form of the expected witness is determined in advance by the contract; a witness might include information such as
* choices among different options or contract features (for example, which of several possible actions the transaction is requesting to take)
* values of specific parameters (for example, an amount)
* asserted state from parties' prior interactions with the contract (see [State Management in SimplicityHL](./state) for more details)
* digital signatures from parties approving the contract or confirming other relevant statements (for example, a party's signatures approving the exercise of some ability under the contract, or an oracle's signature asserting the truth of some off-chain fact such as a market price or whether a specific event has occurred)
The [Witnesses in SimplicityHL development](./witness) document explains the concept in more detail; the [`.wit` file reference](./witness-format) talks about the practical mechanics of creating one.
Because Simplicity is formally specified and fully deterministic, every node that examines that transaction will come to exactly the same conclusion about what the result of running the Simplicity program was, without ambiguity.
## How Simplicity programs are triggered or invoked
Simplicity programs are inherently reactive. Because they cannot initiate transactions or other actions independently, they rely entirely on external client software to "drive" the contract state forward.
To interact with a Simplicity contract, end-user software (such as wallets or specialized apps) must be built specifically around that individual contract. The client software acts as the active engine for the contract's passive logic by:
* Generating transactions: Constructing and submitting on-chain transactions required to trigger specific contract functions.
* State tracking: Monitoring on-chain data to determine which actions are valid, and, if necessary, reminding the contract of the relevant state.
* User interface: Providing an interface to explain the current state to the user and allows the user to make choices (like a "refund" versus "spend" action) by translating those choices into transactions.
In this model, the client software proposes actions, while the Simplicity program judges and confirms whether those actions are actually permissible under the contract rules. Therefore, the client software must know enough about these rules and the contract state to propose transactions that will be accepted.
For development purposes, you can also run a Simplicity program locally with `hal-simplicity simplicity pset run` after building a [PSET](../glossary.md#pset) representing the overall transaction within which the program will run. This simulates the Simplicity logic that a node would follow, although nodes can also reject transactions for various other reasons, such as if the input UTXO has already been spent, if sufficient fees are not paid with the transaction, or if spending conditions applicable to some other referenced UTXO are not satisfied.
Below, this document presents several examples of applications of SimplicityHL and describe how the contracts they implement must be "driven" by some kind of end-user software generating and submitting appropriate transactions.
## Distinctive features of Simplicity and its environment
Simplicity is a deterministic functional programming environment. Simplicity programs [don't have access to any form of I/O or network access](https://delvingbitcoin.org/t/delving-simplicity-part-two-side-effects/2091). They can't display a user interface, read or write files, or call network APIs. In fact, they don't even have direct access to the data of the blockchains with which they are integrated. However, Simplicity does provide programs with the ability to *introspect* the currently-proposed transaction in order to find out details related to input and output asset types, amounts, and addresses. For example, a program can use [introspection](../glossary.md#introspection) to require that an asset is sent back to a copy of that same program; it does so by approving only transactions where an output with exactly the same program code receives the asset that was spent in the program's input.
Whenever a user proposes a transaction that would spend (consume as input) an existing UTXO to which a Simplicity program is attached, the proposed transaction makes a claim that the Simplicity program authorizes that UTXO to be spent in the indicated context. Nodes then check this claim by running the program. Most, though not all, programs will check cryptographic information derived from the attached witness data, such as whether one or more digital signatures included there are valid.
A Simplicity program can include several alternative paths reflecting different scenarios or outcomes, and different criteria for approving each one. A simple example is a timeout branch, where assets controlled by the program can be refunded to their original senders, but only after a certain amount of time has elapsed. This can serve as an alternative to the originally intended outcome in which a certain transaction is completed by transferring assets elsewhere. This prevents assets from being stuck inside the contract if some party fails to perform its role.
## Why not perform more complex computations in Simplicity?
Every Simplicity program is run (albeit in pruned form) by *every node* that validates a block containing a transaction spending assets controlled by the program. The computation to validate transactions is expensive; indeed, creators of transactions may be required to pay for it indirectly via fees.
Simplicity programs perform deterministic computations based on publicly-disclosed information. It is useful to have nodes perform computation to validate compliance with financial logic and contractual rules (that determine who is entitled to specific assets). However, computation that isn't necessary for these purposes doesn't need to be done on-chain and replicated by all validators.
For example, a loan might charge interest at a specified rate. In principle, a Simplicity program could compute how the loan balance will change over time based on different repayment schedules, but it doesn't *need* to make such hypothetical future projections in order to calculate the actual loan balance. It also wouldn't be able to output the results of these computations for anyone to see them. The same information can just as easily be computed by client-side software, and this is much more efficient. That computation can be done just once, on the device of the interested user.
In general, anything that doesn't have to happen on-chain should be handled outside of a Simplicity program. That includes any logic or computations that are relevant to user interface for the contract but not critical to its underlying financial logic and disposition of assets.
Many computationally-intensive tasks that must be performed on-chain, like cryptographic operations, can be outsourced to [jets](../glossary.md#jet), allowing the actual calculations to take place in native code.
## Examples
The functionality of three kinds of contracts is examined below in order to illustrate how Simplicity programs can make decisions in order to determine whether to approve proposed transactions.
These examples do not use introspection features, so they don't demonstrate Simplicity's ability to constrain outputs' destinations. Introspection would also provide an alternative way to implement the refund path in the `htlc` contract (constraining the refund payment to be sent to the address of the original sender of an asset, by asserting that an input address and output address match); this version instead hardcodes a key that can be used to authorize refunds, sent to any chosen address.
### p2ms
This program, `p2ms.simf`, is taken from the SimplicityHL examples collection. An [older version of the `bash` quickstart](/getting-started/bash-quickstart) guide provides a recipe for making a Liquid Testnet transaction using this program.
```rust
/*
* PAY TO MULTISIG
*
* The coins move if 2 of 3 people agree to move them. These people provide
* their signatures, of which exactly 2 are required.
*
* https://docs.ivylang.org/bitcoin/language/ExampleContracts.html#lockwithmultisig
*/
fn not(bit: bool) -> bool {
::into(jet::complement_1(::into(bit)))
}
fn checksig(pk: Pubkey, sig: Signature) {
let msg: u256 = jet::sig_all_hash();
jet::bip_0340_verify((pk, msg), sig);
}
fn checksig_add(counter: u8, pk: Pubkey, maybe_sig: Option) -> u8 {
match maybe_sig {
Some(sig: Signature) => {
checksig(pk, sig);
let (carry, new_counter): (bool, u8) = jet::increment_8(counter);
assert!(not(carry));
new_counter
}
None => counter,
}
}
fn check2of3multisig(pks: [Pubkey; 3], maybe_sigs: [Option; 3]) {
let [pk1, pk2, pk3]: [Pubkey; 3] = pks;
let [sig1, sig2, sig3]: [Option; 3] = maybe_sigs;
let counter1: u8 = checksig_add(0, pk1, sig1);
let counter2: u8 = checksig_add(counter1, pk2, sig2);
let counter3: u8 = checksig_add(counter2, pk3, sig3);
let threshold: u8 = 2;
assert!(jet::eq_8(counter3, threshold));
}
fn main() {
let pks: [Pubkey; 3] = [
0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798, // 1 * G
0xc6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5, // 2 * G
0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9, // 3 * G
];
check2of3multisig(pks, witness::MAYBE_SIGS);
}
```
This program includes three hard-coded public keys. Its logic says that any proposed transaction will be approved if, and only if, exactly two digital signatures on the proposed transaction are provided and those signatures were correctly made by the private keys corresponding to any two of those three keys.
Since the signatures are made over the transaction data including the specific input(s) and output(s), it can be presumed that the holders of those keys agree with transferring specific assets controlled by the contract to those specific destinations.
Once an asset has been sent to this contract (that is, a UTXO identifies it as a spending condition), anyone can propose a transaction that would spend that asset. The contract examines the proposed transaction and decides whether it does or does not contain sufficient evidence (based on the presence or absence of valid signatures provided in the witness). It then approves or rejects the transaction on that basis.
??? "Expand for diagram"
```mermaid
flowchart TD
A((Claiming transaction)) -->|Witness| B[p2ms contract]
B --> C[Valid signature count is 0]
C --> D{Sig 1 provided and valid?}
D -->|Yes| E[Valid signature count increases by 1]
D -->|No| F[Valid signature count unchanged]
E --> G{Sig 2 provided and valid?}
F --> G
G -->|Yes| H[Valid signature count increases by 1]
G -->|No| I[Valid signature count unchanged]
H --> J{Sig 3 provided and valid?}
I --> J
J -->|Yes| K[Valid signature count increases by 1]
J -->|No| L[Valid signature count unchanged]
K --> M{Valid signature count equal to 2?}
L --> M
M -->|Yes| N((Approve transaction))
M -->|No| O((Reject transaction))
```
### htlc
This program, `htlc.simf`, is also taken from the SimplicityHL examples collection. It implements a hash-timelock contract, a mechanism often used in cryptocurrency swaps.
```rust
/*
* HTLC (Hash Time-Locked Contract)
*
* The recipient can spend the coins by providing the secret preimage of a hash.
* The sender can cancel the transfer after a fixed block height.
*
* HTLCs enable two-way payment channels and multi-hop payments,
* such as on the Lightning network.
*
* https://docs.ivylang.org/bitcoin/language/ExampleContracts.html#htlc
*/
fn sha2(string: u256) -> u256 {
let hasher: Ctx8 = jet::sha_256_ctx_8_init();
let hasher: Ctx8 = jet::sha_256_ctx_8_add_32(hasher, string);
jet::sha_256_ctx_8_finalize(hasher)
}
fn checksig(pk: Pubkey, sig: Signature) {
let msg: u256 = jet::sig_all_hash();
jet::bip_0340_verify((pk, msg), sig);
}
fn complete_spend(preimage: u256, recipient_sig: Signature) {
let hash: u256 = sha2(preimage);
let expected_hash: u256 = 0x66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925; // sha2([0x00; 32])
assert!(jet::eq_256(hash, expected_hash));
let recipient_pk: Pubkey = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798; // 1 * G
checksig(recipient_pk, recipient_sig);
}
fn cancel_spend(sender_sig: Signature) {
let timeout: Height = 1000;
jet::check_lock_height(timeout);
let sender_pk: Pubkey = 0xc6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5; // 2 * G
checksig(sender_pk, sender_sig)
}
fn main() {
match witness::COMPLETE_OR_CANCEL {
Left(preimage_sig: (u256, Signature)) => {
let (preimage, recipient_sig): (u256, Signature) = preimage_sig;
complete_spend(preimage, recipient_sig);
},
Right(sender_sig: Signature) => cancel_spend(sender_sig),
}
}
```
This program incorporates logic supporting two different outcomes, which are two different kinds of transactions that can potentially be approved in different circumstances. One path is called "complete" and represents a transaction that claims to complete the intended asset transfer. If the contract is satisfied (by someone revealing an appropriate hash preimage as "password") that this can occur, it will approve a transaction that effectuates this transfer. The authorized recipient must also provide a digital signature approving this transaction.
On the other hand, if the underlying asset being transferred is still controlled by the contract after a specified delay (here, of 1000 blocks since the receipt of the asset), the authenticated original sender can request a refund, and the contract will approve a transaction that effectuates the refund.
In each of these cases, the appropriate party must actively make a claim by submitting a transaction and substantiating it with a [witness](../glossary.md#witness) that proves all required conditions are met. Until the recipient explicitly creates and submits this claiming transaction, the assets remain controlled by the contract.
The contract does not store any kind of state to record whether one or the other paths has already previously been taken. The reason that one path excludes the other is simply that the underlying asset will already have been spent. In this case, the blockchain's transaction validity logic forbids double-spending the same [UTXO](../glossary.md#utxo). Another way of thinking of this is that, after the asset has been claimed from the contract by someone, the contract no longer controls the disposition of that asset, and therefore it is no longer interesting or relevant whether the contract would "agree" to some other transfer. In a certain sense, Simplicity contracts do not "know" what assets they control, but that information is readily available on the blockchain for inspection by software like wallet apps.
??? "Expand for diagram"
```mermaid
flowchart TD
A((Claiming transaction)) -->|Witness| B[htlc contract]
B -->Q{Which action?}
Q -->|Transfer| C{Hash preimage correct?}
Q -->|Refund| D{Time 1000 blocks after input transaction?}
C -->|Yes| E{Recipient signature valid?}
C -->|No| F((Reject transaction))
D -->|Yes| G{Sender signature valid?}
D -->|No| F((Reject transaction))
E -->|Yes| H((Approve transaction))
E -->|No| J((Reject transaction))
G -->|Yes| K((Approve transaction))
G -->|No| L((Reject transaction))
```
### Prediction market
This example discusses a prediction market contract but does not provide an example of SimplicityHL code for this contract.
A prediction market contract provides an example of how Simplicity contract updates are "driven" by some kind of end-user software such as a wallet or a contract-specific app, which must generate and submit appropriate transactions and witness data under appropriate circumstances.
A typical prediction market issues pairs of tokens called YES and NO with respect to a specific question. The market has functionality that tends to ensure that the YES and NO prices remain consistent with one another.
The implementing contract usually provides the following actions:
* Issue pair: lock $1 with the contract; receive new YES and NO tokens
* Redeem pair: burn existing YES and NO tokens; receive locked $1
* Claim YES: burn existing YES token, provide oracle statement asserting that question resolved YES; receive locked $1
* Claim NO: burn existing NO token, provide oracle statement asserting that question resolved NO; receive locked $1
Users can also directly trade YES and NO tokens with one another, allowing their prices to vary from the assumed "indifference" level of $0.50.
At least the final three actions will likely need to be provided by different code paths of the same program, because they all need to be able to release (authorize spending of) some $1 of locked value, and assets controlled by the prediction market ought to be fungible.
If the underlying question resolves as YES, the YES token will typically be worth one currency unit (such as $1), while the NO token will not be redeemable for any value. Conversely, if the underlying question resolves as NO, the NO token will be redeemable for $1 and the YES token will not be redeemable. When the question resolves (by the issuance of a signed [oracle](../glossary.md#oracle) statement indicating which side has won), each "winner" holding a token for the successful position on the question must individually proactively claim a reward by explicitly submitting a transaction that claims $1 from the contract in exchange for consuming a token. Therefore, all of the winners need to have, and use, software capable of formulating this claim transaction in order to receive any benefit from their successful bets in the market. In the absence of a specific claim transaction, the contract does not have any inherent notion of who the winners are or the fact that they have won or are entitled to anything. Some implementations may not even "remember" which side has won, and have to be reminded by resubmitting the oracle statement together with each successive claim.
??? "Expand for diagrams"
```mermaid
sequenceDiagram
participant wallet@{"alias": "User wallet"}
participant node@{"alias": "Node"}
participant impl@{"alias": "Node's Simplicity implementation"}
participant mempool@{"alias": "Mempool"}
wallet->>node: New tx spending assets from UTXO Y to address Z, witness W
node->>impl: Run program P with UTXO Y to address Z, witness W
impl->>node: Success
node->>mempool: This tx is valid, can relay it or include it in a block
node->>wallet: Your tx is valid
```
```mermaid
sequenceDiagram
participant wallet as User wallet
participant node as Node
participant impl as Node's Simplicity implementation
participant mempool as Mempool
wallet->>node: New tx spending assets from UTXO Y to address Z, witness W
node->>impl: Run program P with UTXO Y to address Z, witness W
impl->>node: Failure
node->>wallet: Your tx is invalid
```
### Covenants & State Management
# Covenants and state management
This document describes concepts and features for enforcing complex financial logic in a Simplicity [*covenant*](../glossary.md#covenant) that manages assets across a series of multiple transactions.
## Covenants
Some Simplicity contracts will be "finished" after a single transaction in which some party successfully claims the assets held by the contract. The [Hash Time-Locked Contract](https://github.com/BlockstreamResearch/SimplicityHL/blob/master/examples/htlc.simf) is an example where the contract is complete as soon as an authorized party claims its underlying value.
However, a key feature of Simplicity is the ability to implement more complex financial logic by [introspection](../glossary.md#introspection). Introspection allows a [smart contract](../glossary.md#smart-contract) to enforce policies and relationships that last beyond a single transaction. This is usually done by means of [covenants](../glossary.md#covenant), smart contracts that enforce that assets are sent *back to a copy of the same contract*. This allows a contract to "hold onto" assets across a series of transactions, possibly moving some portion of those assets in or out of the contract, or updating the contract's state over time.
```mermaid
flowchart LR
loop1[ ]:::inv
loop2[ ]:::inv
Z((User A)) -->|Asset deposit| A[Covenant]
Y((User B)) -->|Asset deposit| A
A --- loop1 -->|Some update action| A
A --- loop2 -->|Some other update action| A
A -->|Authorized withdrawal| B((Beneficiary))
A -->|Authorized refund| Y
classDef inv display:none,height:0,width:0;
```
In Simplicity, most complex and long-term financial relationships among multiple parties will be modeled as covenants.
A simple example is provided in [`last_will.simf`](https://github.com/BlockstreamResearch/SimplicityHL/blob/master/examples/last_will.simf).
???+ "Click to hide source code"
```rust
/*
* LAST WILL
*
* The inheritor can spend the coins if the owner doesn't move the them for 180
* days. The owner has to repeat the covenant when he moves the coins with his
* hot key. The owner can break out of the covenant with his cold key.
*/
fn checksig(pk: Pubkey, sig: Signature) {
let msg: u256 = jet::sig_all_hash();
jet::bip_0340_verify((pk, msg), sig);
}
// Enforce the covenant to repeat in the first output.
//
// Elements has explicit fee outputs, so enforce a fee output in the second output.
// Disallow further outputs.
fn recursive_covenant() {
assert!(jet::eq_32(jet::num_outputs(), 2));
let this_script_hash: u256 = jet::current_script_hash();
let output_script_hash: u256 = unwrap(jet::output_script_hash(0));
assert!(jet::eq_256(this_script_hash, output_script_hash));
assert!(unwrap(jet::output_is_fee(1)));
}
fn inherit_spend(inheritor_sig: Signature) {
let days_180: Distance = 25920;
jet::check_lock_distance(days_180);
let inheritor_pk: Pubkey = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798; // 1 * G
checksig(inheritor_pk, inheritor_sig);
}
fn cold_spend(cold_sig: Signature) {
let cold_pk: Pubkey = 0xc6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5; // 2 * G
checksig(cold_pk, cold_sig);
}
fn refresh_spend(hot_sig: Signature) {
let hot_pk: Pubkey = 0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9; // 3 * G
checksig(hot_pk, hot_sig);
recursive_covenant();
}
fn main() {
match witness::INHERIT_OR_NOT {
Left(inheritor_sig: Signature) => inherit_spend(inheritor_sig),
Right(cold_or_hot: Either) => match cold_or_hot {
Left(cold_sig: Signature) => cold_spend(cold_sig),
Right(hot_sig: Signature) => refresh_spend(hot_sig),
},
}
}
```
This contract allows an heir to claim an inheritance (held by the contract) after the creator has died. It also enforces a mandatory time delay in the claim process so that the heir can only claim the inheritance when the creator hasn't "refreshed" the contract within the past 180 days.
```mermaid
flowchart TD
Z((Creator's wallet)) -->|Asset deposit| A[Last will covenant]
A -->|Hot key signature Refresh covenant| A
A -->|Cold key signature Creator withdrawal| Z
A -->|Inheritor signature Inherit assets Only for assets held over 180 days| C((Inheritor's wallet))
```
The creator is expected to periodically refresh the contract by sending the contract's assets back to the same contract (via the **hot key**). Importantly, the "hot key" is restricted to authorizing this specific form of transaction: it can only authorize sending assets back to the same contract. (This restriction on the hot key's power, the fact that it can't *remove* assets from the covenant's control, is the part of this example that uses covenant logic.)
The creator's more secure **cold key** isn't needed routinely, but can be used to authorize arbitrary withdrawals if the contract creator no longer wishes to keep certain assets stored inside the contract.
The inheritor's **inheritor key** can authorize arbitrary withdrawals from the contract, but only of [UTXO](../glossary.md#utxo)s that have been held by the contract for at least 180 days. So, whenever the creator refreshes the covenant with a hot key transaction, this period begins anew.
The covenant logic in `last_will.simf` is enforced by ensuring that a specific [output](../glossary.md#output) has a script hash matching the script hash of the [input](../glossary.md#input) from which the Simplicity program is being run. This is determined using the relevant [jet](../glossary.md#jet)s.
## SimplicityHL state management
The `last_will.simf` example above is **stateless**: it doesn't require the contract to actively remember anything. UTXOs' age is represented on the blockchain itself, and the contract automatically blocks the inheritor from transferring fresh UTXOs.
The [execution environment of a Simplicity program](execution-model.md) is highly constrained; specifically, a Simplicity program can't perform any kind of input or output or network access, and can't even directly access the contents of earlier transactions on the blockchain.
Still, complex contracts will often need to enforce multiple related transactions and "remember" facts and details over time. For example, they may need to record the existence or size of a debt, or record whether a certain action has already been taken. How can they do so in Simplicity's transaction-based architecture, without being able to save or load anything corresponding to files or database entries?
The recommended mechanism uses **cryptographic commitments**. These incorporate the state reference into the on-chain address of a copy of the Simplicity program itself, which the program can confirm by [introspection](../glossary.md#introspection) when it is run.
Effectively, this method generates an address for a program in a way that inherently incorporates a cryptographic reference to specific state; when that program is run, it can confirm that the state it was given via a [witness](../glossary.md#witness) matches the state that it expects to have according to its own address. It can immediately reject any proposed transactions that attempt to delete or tamper with the state.
This approach provides a way for a contract to maintain state (the values of specific variables) between one transaction and another transaction, enforcing a guarantee that the later transaction has access to the correct values of those variables and that no one has modified them. The contract does so by enforcing that outputs, including address references to versions of the same contract, contain cryptographic references to that state ("save"). A version of the same or a related contract can then enforce in a later transaction that suggested state provided to that new transaction matches up correctly with those cryptographic references ("load").
## Wallet-side state storage and on-chain verification
This means that the actual state data is not directly stored "inside of" the contract; the contract possesses a reliable way to *verify* state that is provided to it, but that state information is typically physically stored inside of a user's wallet software, and passed back to the contract whenever a new transaction involving the contract is constructed. Web developers may recognize this pattern as akin to digitally signed tokens (such as [JWT](https://www.jwt.io/introduction#what-is-json-web-token)) provided by clients to web applications. In the web application setting, the physical storage of the state information can be offloaded to the client, and a digital signature proves that the client didn't modify its contents. The "client" (the wallet or other software that is constructing future transactions) similarly has the responsibility to store and provide the state information back to the contract, under a form of cryptographic authentication preventing modification, although the exact cryptographic details are different from the JWT analogy.
In the Simplicity context, the cryptographic commitment to the state is actually used as part of the contract's on-chain address, so performing a state update will actually mean deriving an updated address for the same contract (or a specifically chosen successor contract), and committing a transaction that forwards assets from the contract's prior address to the updated address. Those forwarded assets' spending conditions are then controlled by the updated version of the contract, which is cryptographically bound to the updated version of the state information.
Whenever a [covenant](../glossary.md#covenant) performs a state update, it sends assets to a new copy of itself with a *different on-chain address* encoding a reference to the updated state data. A transaction can update state without moving any assets in or out of the contract, merely sending them to the new copy with the new address. Client software meant to interact with the contract must be programmed to observe that this has happened so that new transactions are always performed with the most current updated copy.
Although wallet software should generally store contract state in order to provide it back to the contract when performing subsequent transactions, this information could be recalculated if necessary by examining blockchain history, because all Simplicity program execution is deterministic and based on public information. Thus, state information isn't generally confidential; locally replaying the evolution of a contract will ordinarily reveal what the expected state for the next transaction should be. (An initial state commitment when a copy of a program receives assets for the first time could require witness values that have not yet been publicly revealed anywhere, much as the code of the program may not have been publicly revealed. Storing state in a Merkle tree also optionally permits the state to be partially revealed as it is actually needed. For example, state of one branch of the program could be stored in one branch of the Merkle tree and state of another branch could be stored in another branch. The program could be designed so that the witness only reveals the part of the state that is actually used.)
## Basic state management mechanism
The modified address is calculated by storing a 256-bit state value in [Taproot](../glossary.md#taproot) alongside a commitment to the Simplicity program's code. Sample code to assert that input state is consistent with the program's address ("load"), and to assert that an output address is consistent with a commitment to a specific updated state value ("store") appears below. The `hal-simplicity simplicity pset update-input` command has also been updated with a `-s` flag that provides an input state value to the program; a copy should also be provided in the witness as `witness::STATE`.
The Rust version of this logic is found in [`state_management/bytes32_tr_storage`](https://github.com/BlockstreamResearch/simplicity-contracts/tree/main/crates/contracts/src/state_management/bytes32_tr_storage), including Rust code to build witnesses and transactions. This shows how a wallet can actually track and provide state back to the contract in a subsequent transaction.
Currently, this allows a program, if structured as a [covenant](../glossary.md#covenant), to pass itself state updates across subsequent transactions. The state information is always represented as a single uninterpreted `u256` integer value. This is conveniently the size of the output of a SHA256 hash, so a program can choose to interpret this value as a SHA256 hash of specified data items that are provided in a [witness](../glossary.md#witness), in a specific order. The program can then commit to specific values of these chosen data items between one transaction and the next. A more elegant approach would be interpreting this `u256` value as a reference to the root of a [Merkle tree](../glossary.md#merkle-tree).
A discussion and demonstration of this approach took place in [the December 23, 2025 Simplicity Office Hours session](https://youtu.be/ry2wQelP8Kc).
## Example with integer counter
This example contract, `third_time.simf`, uses the state management mechanism described in the prior section. It enforces the saying "the third time's the charm"; it requires a series of three distinct transactions in order to perform a withdrawal.
???+ "Click to hide source code"
```rust
/*
* "Third Time's The Charm" covenant demonstrating Simplicity state management
*
* This covenant requires three transactions in a row in order to release the
* locked coins. It counts how many of these transactions have been seen so
* far using the witness::STATE value.
*
* State management:
* Computes the "State Commitment" — the expected Script PubKey (address)
* for a specific state value.
*
* HOW IT WORKS:
* In Simplicity/Liquid, state is not stored in a dedicated database. Instead,
* it is verified via a "Commitment Scheme" inside the Taproot tree of the UTXO.
*
* This function reconstructs the Taproot structure to validate that the provided
* witness data (state_data) was indeed cryptographically embedded into the
* transaction output that is currently being spent.
*
* LOGIC FLOW:
* 1. Takes state_data (passed via witness at runtime).
* 2. Hashes it as a non-executable TapData leaf.
* 3. Combines it with the current program's CMR (tapleaf_hash).
* 4. Derives the tweaked_key (Internal Key + Merkle Root).
* 5. Returns the final SHA256 script hash (SegWit v1).
*
* USAGE:
* - For load(), we verify: CalculatedHash(witness::STATE) == input_script_hash.
* - For store(), we verify: CalculatedHash(updated_state) == output_script_hash.
* - This assertion proves that the UTXO is "locked" not just by the code,
* but specifically by THIS instance of the state data. So the on-chain address
* necessarily changes after each state-updating transaction in order to reflect
* a cryptographic commitment to the appropriate state data.
*
* Surrounding context tracks the state_data, interpreting its low 64 bits as
* a counter. The counter can be updated by an "update" transaction sending
* value back to the same contract. When the counter is equal to 2 or more,
* the "withdraw" transaction is permitted, sending some or all value
* to an arbitrary output address instead of back to the contract. Both the
* "update" and "withdraw" transactions must include a valid signature by
* the authorized key (here hardcoded in check_sig() for demonstration
* purposes).
*
* When initially funding the contract, use witness::STATE = 0.
*/
fn check_sig(sig: Signature) {
let authorized_key: Pubkey = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798; // 1 * G
let msg: u256 = jet::sig_all_hash();
jet::bip_0340_verify((authorized_key, msg), sig);
}
fn script_hash_for_input_script(state_data: u256) -> u256 {
// This is the bulk of our "compute state commitment" logic from above.
let tap_leaf: u256 = jet::tapleaf_hash();
let state_ctx1: Ctx8 = jet::tapdata_init();
let state_ctx2: Ctx8 = jet::sha_256_ctx_8_add_32(state_ctx1, state_data);
let state_leaf: u256 = jet::sha_256_ctx_8_finalize(state_ctx2);
let tap_node: u256 = jet::build_tapbranch(tap_leaf, state_leaf);
// Compute a taptweak using this.
let bip0341_key: u256 = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0;
let tweaked_key: u256 = jet::build_taptweak(bip0341_key, tap_node);
// Turn the taptweak into a script hash
let hash_ctx1: Ctx8 = jet::sha_256_ctx_8_init();
let hash_ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(hash_ctx1, 0x5120); // Segwit v1, length 32
let hash_ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(hash_ctx2, tweaked_key);
jet::sha_256_ctx_8_finalize(hash_ctx3)
}
fn load(state_data: u256) {
// Assert that the input is correct, i.e. "load".
assert!(jet::eq_256(
script_hash_for_input_script(state_data),
unwrap(jet::input_script_hash(jet::current_index()))
));
}
fn store(new_state: u256) {
assert!(jet::eq_256(
script_hash_for_input_script(new_state),
unwrap(jet::output_script_hash(jet::current_index()))
));
}
fn update(sig: Signature, state_data: u256) {
// In this case, if the signature is correct, we approve the transaction
// if the destination is the same contract but with a correct internal
// state update that increases the count by 1.
// Check that the signature is correct.
check_sig(sig);
let (state1, state2, state3, count): (u64, u64, u64, u64) = ::into(state_data);
let (carry, new_count): (bool, u64) = jet::increment_64(count);
// Check for overflow.
assert!(jet::eq_1(::into(carry), 0));
// Assert that the output is being sent to a correctly-updated copy of this
// specific program, i.e. "store".
let new_state: u256 = <(u64, u64, u64, u64)>::into((state1, state2, state3, new_count));
store(new_state);
// Assert that there are exactly two outputs in the currently-proposed
// transaction (corresponding to the new contract and the network fee
// payment). Without this logic, the updater could cause coins to leak out
// of the covenant by sending some of the input value to an uncontrolled
// output address that is not a copy of this contract. (Even with this
// logic, the fee amount itself is not constrained here, and the updater
// could choose to give away some or all of the stored value to miners in
// the form of an excessive fee.)
assert!(jet::eq_32(jet::num_outputs(), 2));
assert!(unwrap(jet::output_is_fee(1)));
}
fn withdraw(sig: Signature, state_data: u256) {
// In this case, if the signature is correct, and the count is already at
// least 2, we approve the transaction (allowing the destination(s) and
// amount(s) indicated by the proposer).
// Check that the signature is correct.
check_sig(sig);
// Assert that the count from the provided state is already at least 2.
let (_, _, _, count): (u64, u64, u64, u64) = ::into(state_data);
assert!(jet::le_64(2, count));
}
fn main() {
// Assert that the provided state_data is correct according to this
// program's cryptographic commitment.
let state_data: u256 = witness::STATE;
load(state_data);
match witness::UPDATE_OR_WITHDRAW {
Left(sig: Signature) => update(sig, state_data),
Right(sig2: Signature) => withdraw(sig2, state_data),
}
}
```
The use of `jet::le_64` means that one can update *at least* twice before a withdrawal. If this is replaced by `jet::eq_64`, the contract will enforce *exactly* two updates before a withdrawal.
As described earlier, each of the intermediate transactions will be sent to a **different on-chain address** representing a commitment to the same Simplicity covenant, but with different state. Thus, the `update` transactions in the diagram below each have corresponding distinct destination addresses.
```mermaid
flowchart TD
A((Faucet)) -->|Deposit| B[Contract count=0]
B -->|Update| C[Contract count=1]
C -->|Update| D[Contract count=2]
D -->|Withdraw| E((User wallet))
```
### Constraining state updates
Contracts must include logic to ensure that only authorized parties can perform transactions that cause state updates. The contract above does this with `check_sig()`, requiring that all transactions be signed by an authorized party.
The `update()` function in this example enforces further constraints on outputs, such as constraining the total number of outputs to exactly two. The first output must be an updated version of the same contract, while the second output must be a network fee payment. Without this constraint, the updater could add an additional output that leaks contract funds to an unrelated address.
A real financial application may need to include logic to enforce a variety of further constraints. For example:
* The contract may be designed to deal with a specified [asset](../glossary.md#asset), such as Liquid bitcoin (LBTC). Since [Liquid](../glossary.md#liquid) supports a variety of assets, the contract should inspect the assets being transferred to confirm that they always of the expected kinds. In most contexts, payments should not be allowed to be made with arbitrary assets.
* The contract may need to constrain fee amounts in order to prevent parties from intentionally donating the contract's assets to miners via excessive fees. The contract may also require a party performing an update to pay the network fee. In that case, it must allow that party to provide a fee input in the transaction that covers the transaction fee, and may also allow the party to receive change from the fee input.
## Next steps
As noted above, a program can store multiple values by interpreting the commitment as a SHA256 hash, where the `load()` and `store()` routines verify that the hash value matches the hash calculated from all the relevant witness values. In this case, the contract developer will create a convention for exactly which values are hashed and in which order; that convention is then used whenever state is stored or loaded.
SimplicityHL will have library functions reflecting the `load` and `store` patterns to make it easier for developers to use these mechanisms without writing boilerplate referring to the lower-level details of the cryptographic state commitment. It will also offer a reference implementation of Merkle tree creation and verification to make it easier for developers to load and store multiple values in a standardized and elegant way.
### Witnesses
# Witnesses in SimplicityHL development
This document describes *witnesses*, which are transaction-time input data for a Simplicity contract provided by the user proposing the transaction. A witness explains what the user wants the contract to do, and convinces the contract that this action is authorized.
When you interact with a Simplicity [contract](../glossary.md#contract) on the blockchain, you'll need to build and attach witness data for each [transaction](../glossary.md#transaction).
The [execution model](../execution-model) for Simplicity [contract](../glossary.md#contract)s allows the user who is proposing a [transaction](../glossary.md#transaction) to provide input values to the contract. Each contract expects different inputs, but in general they help confirm that the proposed transaction is authorized according to the contract's rules. This is necessary because anyone can propose transactions to spend assets at any time, so a contract needs a clear way to distinguish which transactions are appropriate and which aren't.
One can think of a Simplicity program as a function that deterministically answers "yes" or "no" to each proposed transaction. The input data for this function will be the specific transaction details, together with some user-supplied inputs which are collectively known as a *witness*. The form of the expected witness is determined in advance by the Simplicity program, just as any function definition determines what kind of input that function expects.
The term "witness" here is adopted from its existing use in other kinds of Bitcoin transactions, and originally from a related meaning in computer science.
Among other things, a witness will usually contain digital signatures from some party or parties approving the proposed transaction. It might also include things like
* amounts (for example, how much of an asset is requested to be spent or transferred)
* oracle statements (confirming some fact about the outside world)
* values representing *choices* among several actions that can be taken at a certain moment (for example, whether a payment should proceed or be cancelled and refunded).
The witness is directly attached to the transaction and forms a part of it; if the transaction is confirmed, the witness data will be publicly visible on the blockchain as part of the confirmed transaction.
In an end-user application, witness data will typically be built by wallet or app software that understands how to interact with a certain contract on the user's behalf. During the contract development process, developers might build it manually.
Please note that this document is discussing "inputs" informally in the typical software development sense of [data provided to a function or program](https://en.wikipedia.org/wiki/Parameter_(computer_programming)), not the blockchain-specific sense of the specific [UTXO](../glossary.md#utxo)s consumed by a transaction (which will also be details relevant to many contracts' logic).
For the practical mechanics of building `.wit` files, compiling them with `simc`, and formatting every SimplicityHL type as a witness value, see the [`.wit` file reference](witness-format.md).
### Oracles
# Oracles in Simplicity
[Smart contracts](../glossary.md#smart-contract) in Simplicity, like other on-chain smart contracts, can only directly access or observe [transaction](../glossary.md#transaction) data.
However, contracts often refer to off-chain facts, such as a market price or whether an event has happened.
* What is the price of Bitcoin in dollars (on a specified exchange)?
* What is the price of wheat in Euros (on a specified market with specified delivery terms)?
* Who won the 2026 Super Bowl?
* Did the insured party suffer a covered loss during the term of an insurance contract? If so, how large was that loss?
These facts can be communicated to a contract with the help of an *oracle*, which is simply a trusted party that can make a digitally signed statement that the contract will accept.
Any entity that all of the parties interested in a contract can trust, and that's willing to make digital signatures in a prearranged form, can be an oracle.
The oracle needs to publish a public key and a well-defined format or structure that its statements will follow. These technical details are then encoded into any contract that will rely on that oracle.
When the oracle makes a statement, it communicates the signed statement publicly, or to a party that relies on it. That party then provides the relevant signed oracle statement and signature *in the [witness](../glossary.md#witness)* to a transaction involving a Simplicity contract.
The Simplicity contract receives the signed statement and signature via the witness and can verify their correctness and make a decision based on the content of the oracle statement.
## Examples of oracle statements
For example, an oracle for a yes-or-no event might say:
> We will hash one of the following `u8` values
> `0` representing NO
>
> `1` representing YES
>
> with SHA256 and we will then sign the result with BIP0340 with the key corresponding to public key `0x292cbaf344fc104ff2307cce72e84f13dbfeb6f603fe94938208c6150733b910`.
In this case the oracle could issue the signature `56c9b207945aca49302e26e0f68c7f28ce2801be1c8ed2eb0421d257ab6bf818be854a0a5d08a7346a6ec753c71aaf3c2ec346202ea061f32300f2da81493b40` (a BIP0340 signature of `SHA256(0x00)`) in case the underlying event turns out as NO.
Or a price oracle might say
> We will construct an oracle statement by hashing a `Height` (representing a [Liquid Network](../glossary.md/#liquid) [block height](../glossary.md#height) followed by a `u32` (representing a number of U.S. dollars that were paid on an exchange for 1 BTC in the most recent exchange transaction) with SHA256. We will sign this statement with the BIP0340 key corresponding to `0x292cbaf344fc104ff2307cce72e84f13dbfeb6f603fe94938208c6150733b910`.
The oracle may then publish the values `(height, price, signature)`, representing an instance of such an oracle statement. For example, it might publish
> (`3793375, 70664, 0x63decfbfb76f4b549d840d9c1fc95bf2bbe4d82209cf03d27fd4aef7e92d8d20f6844076588058baf70f34ee7e7a7ac2b44d0db2e1be7a792a474cc1d0544a6f`)
attesting to a price of $70664 observed at the time of Liquid block 3793375.
!!! Note
It's important for the oracle to clearly define the format of its statement, including the specific data types of the included values, in order to prevent any ambiguity about the oracle statement's meaning. For example, it must not be possible to reinterpret part of the block height as part of the price, or vice versa. This example avoids ambiguity by specifying that the height is a `Height` (32 bits) and that the price is a `u32` (32 bits). This makes it clear what each individual bit in the signed statement means.
Then a Simplicity witness using this statement might include
```json
{
"oracle_height": {
"value": "3793375",
"type": "Height"
},
"oracle_price": {
"value": "70664",
"type": "u32"
},
"oracle_signature": {
"value": "0x63decfbfb76f4b549d840d9c1fc95bf2bbe4d82209cf03d27fd4aef7e92d8d20f6844076588058baf70f34ee7e7a7ac2b44d0db2e1be7a792a474cc1d0544a6f",
"type": "Signature"
}
}
```
A SimplicityHL program that's run with this witness can verify it with
```rust
// Minimum required block height (= 2026-03-09).
// Block height is a 32-bit value.
assert!(jet::lt_32(3790000, witness::oracle_height));
// Hardcoded key of trusted signer
let oracle_pubkey: Pubkey = 0x292cbaf344fc104ff2307cce72e84f13dbfeb6f603fe94938208c6150733b910;
let ctx: Ctx8 = jet::sha_256_ctx_8_init();
let ctx1: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, witness::oracle_height);
let ctx2: Ctx8 = jet::sha_256_ctx_8_add_32(ctx1, witness::oracle_price);
let overall_hash: u256 = jet::sha_256_ctx_8_finalize(ctx2);
jet::bip_0340_verify((oracle_pubkey, overall_hash), witness::oracle_signature);
```
Note that the program recomputes the SHA256 hash of the asserted data values, then checks that the provided oracle signature is a valid signature for that hash. If a party tried to submit a false or modified oracle statement, the oracle's signature wouldn't verify correctly.
The program can then use the validated `witness::oracle_price` value for other logic (for example, calculating a liquidation threshold or a pro-rated refund amount in the [Simplicity Lending Protocol](../../use-cases/lending-protocol), or determining whether it exceeded a threshold for the conditions of a prediction contract).
A contract can also allow for multiple oracles by accepting any of several different public keys as trusted, providing a different verification path for each key.
## Prediction market example
Resolvr's [Deadcat project](https://github.com/Resolvr-io/deadcat/) includes an example of verifying oracle statements as part of a prediction market implementation. You can see the oracle statement verification logic itself in `verify_oracle_signature()` in [`prediction_market.simf`](https://github.com/Resolvr-io/deadcat/blob/master/src-tauri/crates/deadcat-sdk/contract/prediction_market.simf). A party claiming a redemption from the resolution of a prediction market position needs to provide a matching oracle statement to the contract.
## Oracles for business logic integration
In addition to financial applications like prediction markets and options contracts, oracles have applications for letting smart contracts "query" a company's databases or APIs. In this capacity, an internal oracle can bridge the gap between business systems and the blockchain by making signed statements about the results of API calls.
When a contract needs access to information kept in a database in order to make a decision, oracle mechanisms help work around [the fact that Simplicity contracts can't directly access off-chain data](../../documentation/execution-model).
For example, suppose you're creating an application which is only allowed to send withdrawals to explicitly pre-approved addresses. But the list of such addresses may live in a database, not on the blockchain. An internal oracle can help by
1. performing a database lookup,
2. combining the results with a current timestamp and,
3. digitally signing the results.
If a specific address is approved for withdrawals, that oracle statement confirms this fact in a format that can then be provided to the smart contract. If it's not, the oracle statement won't be issued and the transaction won't be completed.
Any kind of business may find this useful for adding back-end business logic to a smart contract application (in exchange for imposing dependencies on transactions). It may be especially relevant to regulated entities building regulated financial products and services, where the creators may want to apply additional off-chain criteria for approval of some on-chain actions.
??? Example
A user wants to withdraw assets from a smart contract to a user-specified address. The user has previously registered this address with a bank that created the contract and is using it to provide an on-chain service.
The user's software contacts the bank and requests an oracle statement to confirm that this transaction is allowed. The bank's systems perform a database lookup, find that the address is approved, and sign a timestamped oracle statement saying so. Now the user's software can provide this oracle statement to the smart contract, along with other user authorization information, to let the contract know that it's allowed to process the withdrawal.
If the address isn't found in the database, the bank's systems decline to issue the oracle statement, and instead let the user know what to do in order to get the address authorized to receive withdrawals.
For bidirectional communications between back-office business systems and a Simplicity smart contract, an indexing system can track on-chain transactions that affect smart contract state, and report the associated state updates to a traditional database.
## The oracle backend
At a technical level, issuing oracle statements for Simplicity contracts requires the following pieces:
* Generating a BIP0340 keypair
* An unambiguous format for structuring oracle statements
* Determining the underlying facts to which the oracle will attest
* Generating oracle statements based on the ascertained facts
* SHA256 hashing
* BIP0340 signing
* Publishing the oracle statements and associated signatures
SHA256 and BIP0340 should generally be used because they're most compatible with the rest of the ecosystem, and because Simplicity has built-in [jets](../glossary.md#jet) to help verify signatures made with them.
!!! warning
Operating a public oracle that smart contracts rely on is a serious responsibility, and may expose the operator to attacks meant to steal its keys or induce it to issue false statements. Oracle operators' security and business continuity best practices are beyond the scope of this document.
## Timeouts
If an oracle completely ceases to operate, it might be impossible to resolve contracts that rely on that oracle's statements. So, many forms of contract that rely on oracles can benefit from a timeout mechanism as a fail-safe. The smart contract can provide a timeout path so that, after a significant period of time has passed after the contract should have been resolved, unclaimed funds can be claimed as a refund by their original senders, or to some predesignated beneficiary.
With this precaution in place, if a relevant oracle statement is never produced, funds are not locked inside the contract permanently.
### Timelocks
# Timelocks
Timelocks are a scripting feature of Bitcoin and inherited by [Elements](../glossary.md#elements). Timelocks can be used in Simplicity and SimplicityHL to enforce a rule that a [transaction](../glossary.md#transaction) may only happen after a specified amount of time has passed.
This is useful for *timeout* logic. Contracts will often have a timeout branch as a fallback mechanism. This timeout branch can allow parties to receive a refund of their assets if the [contract](../glossary.md#contract) is not successfully completed. For example, an atomic swap contract has a timeout where the initiator can be refunded if the other party doesn't complete the swap in a specified period of time.
A [vault contract](../glossary.md#vault) can also use timelocks to require a series of transactions, with a specific time delay between them, in order to achieve a withdrawal.
## Timelock measurement units
Timelocks can be expressed in various ways. An *absolute* timelock gives an absolute time after which a transaction may occur ("starting next Monday"), while a *relative* timelock specifies how much later a transaction may occur after a prior transaction ("at least one week after this input was confirmed").
Time can be expressed in terms of *blocks* on the blockchain. A blockchain consists of an ever-growing series of blocks, which can be counted to obtain an ever-increasing timescale. In Simplicity, an absolute timelock measured in blocks is [called](../../simplicityhl-reference/type_alias/) a `Height`, while a relative timelock measured in blocks is called a `Distance`.
!!! note "Block creation intervals"
**[Liquid Network](../glossary.md#liquid) blocks are created once per minute.** This is different from Bitcoin, where a block is created (on average) once every ten minutes.
For example, [Liquid block 3800000](https://liquid.network/block/59d271b0df9808eb59e686be30bf0e3c3faf121a72dbd751e90df7bcaed90533) was created at 12:08:10 UTC on March 16, 2026.
Time can also be expressed in terms of *real time*. In Simplicity, an absolute timelock measured in real time is called a `Time`, while a relative timelock measured in real time is called a `Duration`.
!!! note "Absolute time scale"
In both Bitcoin and Liquid, **the *real time* scale for absolute timelock measurement is [*Unix time*](https://en.wikipedia.org/wiki/Unix_time)** (seconds since January 1, 1970).
For example, `Time` 1234567890 occurred on February 13, 2009, while `Time` 1800000000 will occur on January 15, 2027.
!!! note "Relative time scale"
In both Bitcoin and Liquid, **the units of *real time* for relative timelock measurement are *intervals of 512 seconds***.
Note that the unit of `Duration` is 512 seconds, while the increment of `Time` is 1 second.
For example, one day of real time is slightly less than 169 units of `Duration`.
### Summary
| Item | Absolute/Relative | Type name | Unit/scale |
| ----| --- | --- | --- |
| Blocks *(specified block height)* | Absolute | `Height` | Blockchain blocks |
| Blocks *(block count interval)* | Relative | `Distance` | Blockchain blocks |
| Real time *(specified time)* | Absolute | `Time` | Unix timestamp (seconds since 1970) |
| Real time *(interval)* | Relative | `Duration` | Units of 512 seconds |
You can pick any of these forms of timelock to use in any context in a Simplicity contract. Do not enforce more than one simultaneously, as it may be impossible to verify more than one form of timelock within a single transaction.
## Timelock enforcement mechanisms
When a [UTXO](../glossary.md#utxo) enforces a timelock, any transaction that attempts to spend resources from that UTXO must *assert* its compliance with the timelock. This is done by setting the `sequence` or `locktime` fields inside the transaction.
The timelock enforcement is a two-step process.
(1) The timelock-related [jets](../glossary.md#jet) in Simplicity can read the fields from the transaction and determine whether they obey the applicable timelock requirements.
(2) The blockchain consensus rules (applied by [nodes](../glossary.md#node) verifying transactions) will reject a transaction that asserts `sequence` or `locktime` fields that are still in the future compared to the current block.
Thus, Simplicity logic checks "is my timelock rule satisfied by this transaction's `sequence` or `locktime`?"; blockchain consensus checks "according to its `sequence` or `locktime`, is this transaction acceptable to include in the blockchain yet?".
??? "Detailed example"
Consider a SimplicityHL contract that, at some point, enforces a minimum relative `Distance` of 100 blocks. (A concrete version of the required code appears further below.)
If you want to build a transaction claiming assets from this contract, you will need to set the `sequence` field on each input to at least 100. Suppose there is only one input to your transaction. Then...
* If you don't set `sequence` at all, it will be assumed to be 0, and the timelock enforcement code further below will fail.
* If you set `sequence` to 50, the timelock enforcement code will fail because the `sequence` value is smaller than required.
* If you set `sequence` to 150, the timelock condition *succeeds*. However, if you *submit* this transaction to the Liquid Network blockchain less than 150 minutes after the input transaction, the [node](../glossary.md#node) to which you submitted it will reject it with a `non-BIP68-final` (meaning that it's still too early for the proposed transaction to be valid).
* If you set `sequence` to 150 and submit the transaction at least 150 blocks after the input transaction, the transaction should be valid and accepted on the blockchain (at least as far as the timelock condition is concerned!).
!!! note
Note that the timelock jets can't "look up" the real time in the outside world. Rather, they can look up *minimum* times that the transaction creator claimed the transaction would be submitted.
In lower-level libraries and documentation, these fields are also called `nSequence` and `nLockTime`.
## Absolute timelock in SimplicityHL
Enforcing an absolute timelock in SimplicityHL uses the jets `check_lock_height` (for absolute block height) or `check_lock_time` (for absolute Unix time).
`jet::check_lock_height(min_height: Height)`: Assert that the transaction's locktime is a block height greater than or equal to the provided value. Such a transaction cannot be included on the blockchain prior to that block height.
`jet::check_lock_time(min_time: Time)`: Assert that the transaction's locktime is a Unix timestamp strictly greater than or equal to the provided value. Such a transaction cannot be included on the blockchain prior to that timestamp.
## Relative timelock in SimplicityHL
A relative timelock is calculated based on the time when a particular UTXO was included in a block.
!!! warning "Deprecated jets"
The Simplicity jets that directly enforce relative timelocks have been deprecated due to an implementation error. However, a workaround is available. This document describes the workaround, not the deprecated jets.
This function enforces a relative distance timelock by calling a combination of related jets.
```rust
fn enforce_relative_distance(min_distance: Distance) {
// Assert that the current input is spent in a transaction that can
// only appear a distance of at least min_distance blocks after the input's
// UTXO. Panic otherwise.
// Transaction version must be at least 2.
assert!(jet::le_32(2, jet::version()));
// Fetch and parse sequence for current transaction
let parsed_seq: Option> = jet::parse_sequence(jet::current_sequence());
match parsed_seq {
// Failure condition
None => assert!(false),
// This is either a distance or a duration, but only a distance is
// acceptable here.
Some(actual_data: Either) => match actual_data {
// Is the actual distance greater than or equal to the specified min_distance?
Left(actual_distance: Distance) => assert!(jet::le_16(min_distance, actual_distance)),
// A duration is not acceptable in this context.
Right(actual_duration: Duration) => assert!(false),
},
}
}
```
And this is the equivalent function to enforce a relative duration timelock.
```rust
fn enforce_relative_duration(min_duration: Duration) {
// Assert that the current input is spent in a transaction that can only
// appear a duration of at least min_duration units of 512 seconds after
// the input's UTXO. Panic otherwise.
// Transaction version must be at least 2.
assert!(jet::le_32(2, jet::version()));
// Fetch and parse sequence for current transaction
let parsed_seq: Option> = jet::parse_sequence(jet::current_sequence());
match parsed_seq {
// Failure condition
None => assert!(false),
// This is either a distance or a duration, but only a duration is
// acceptable here.
Some(actual_data: Either) => match actual_data {
// A distance is not acceptable in this context.
Left(actual_distance: Distance) => assert!(false),
// Is the actual duration greater than or equal to the specified min_duration?
Right(actual_duration: Duration) => assert!(jet::le_16(min_duration, actual_duration)),
},
}
}
```
For real-time-based timelocks, Bitcoin and Elements use a rule called [Median Time Past](https://github.com/bitcoin/bips/blob/master/bip-0113.mediawiki) when determining the effective current time. This rule induces an extra delay (six minutes for Liquid Network, or about one hour for Bitcoin) for the validity of a transaction that using a real-time-based timelock.
## Creating appropriate transactions
Transactions that consume UTXOs with timelock conditions must be constructed appropriately to assert the appropriate timelock. The necessary properties can be set as follows:
| Kind | Type name | Transaction property |
| ---- | --- | --- |
| Absolute blocks | `Height` | `nSequence = 0xfffffffe` `nLockTime < 500000000` `nLockTime` equal to desired `Height` |
| Absolute time | `Time` | `nSequence = 0xfffffffe` `nLockTime >= 500000000` `nLockTime` equal to desired `Time` (Unix timestamp) |
| Relative blocks | `Distance` | `nSequence < 0x10000` `nSequence` equal to desired `Distance` (`0` ... `0xffff`) |
| Relative time | `Duration` | `nSequence = 0x00400000` + desired `Duration` (`0` ... `0xffff`) |
!!! note "More details"
The values in the table above are simple heuristics for common timelock assertions and do not include all scenarios and options.
For more details on timelock implementation and semantics, please see the Bitcoin timelock specifications in [BIP-65](https://github.com/bitcoin/bips/blob/master/bip-0065.mediawiki), [BIP-68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki), and [BIP-112](https://github.com/bitcoin/bips/blob/master/bip-0112.mediawiki).
As noted above, transaction-building tools and APIs may simply refer to the fields as `sequence` and `locktime`.
## No maximum time constraints
!!! warning "Timelocks enforce minimum transaction times, not maximum times"
In some smart contracts, one might be tempted to use timelock jets to require a transaction to happen *before* a certain time rather than *after*. However, this is not supported. **Architecturally, timelocks in Simplicity contracts can only be usefully used to enforce minimum times, not maximum times, when transactions can occur.**
This section further discusses this limitation.
Because of the *monotonicity* property of Bitcoin and related blockchain systems, there is no way to directly express a requirement that a transaction occur *before* a certain block height and not after. These systems enforce a rule that a specific transaction that was valid at some point remains valid at all times in the future. Although you can write SimplicityHL code that asserts that a lock distance is *smaller than* rather than *larger than* a specific numerical value, the person creating the transaction can simply assert a small `sequence` value which will be accepted as valid by the blockchain consensus.
For example, if a contract was funded with a specific input at block height 5000, and the contract asserts that the relative `Distance` when spending that input should be *less than* 100, a transaction spending that input while asserting `sequence` equal to 50 will still be valid when committed at block height 10000, as the criteria 50<10000-5000 (required by the nodes enforcing blockchain consensus rules in the transaction sequence) and 50<100 (for the contract's own logic) are both true. Asserting the lock distance is small requires the use of a correspondingly small `sequence` value, but this does *not* imply that the resulting transaction is necessarily committed to the blockchain within a short time after the inputs it consumes.
### A workaround: state updates and preemption
Because permitted transactions cannot automatically expire or become forbidden, Simplicity requires a different approach to enforce deadlines. Instead, you can achieve an *effective* maximum time for an action using a preemption pattern.
The core logic is simple: you can make it impossible to perform an action by removing the assets or state that it relies upon. This requires some party to proactively submit a transaction that performs the preempting action after some time limit has passed. It does not happen automatically.
A simple example is a timeout-and-refund mechanism. A contract can provide that, after a certain time period, assets may be transferred back to their original senders. Once a sender proactively claims such a refund, no further actions can be taken with the associated assets because the contract no longer holds them.
In general, a preempting transaction transfers the assets elsewhere, or it updates the [covenant's](../glossary.md#covenant) [state](../state.md) to a new version that restricts previously allowed actions. A covenant can be written to permit an authorized party to perform a state update at a certain time which causes previously permitted actions to be forbidden. For example, an authorized update could perform an internal state change to declare a contract "closed" to new claims after a deadline.
An effective deadline is enforced provided that a party is incentivized to perform the required transfer or update action.
### Simplicity SDK (Simplex)
# Simplex
Simplex is a development framework and orchestration tool for SimplicityHL smart contracts. It assumes a Rust development environment, and helps you create applications and tools in Rust to interact with the underlying contract.
Simplex facilitates creating and running realistic smart contract integration tests, providing a straightforward way to define and run a suite of such tests in Rust. It automatically runs tests against a local instance of `elementsd`, creating a customizable [Liquid Network](../glossary.md#liquid)-like environment for test transactions. Thus, tests can be run without broadcasting transactions publicly on Liquid testnet.
A Simplex project's `Simplex.toml` file (automatically created by `simplex init`, as described below) configures the details of this test environment.
## Installing Simplex
You can get Simplex from [https://github.com/BlockstreamResearch/smplx](https://github.com/BlockstreamResearch/smplx), or use the auto-install script:
```bash
curl -L https://smplx.simplicity-lang.org | bash
simplexup
```
When Simplex is installed with `simplexup`, you can rerun `simplexup` at any time to upgrade to the most recent release.
## Using Simplex
### Creating a project
Run `simplex init` at the top-level directory of the project. This is ordinarily the same as the top-level directory of the associated Rust project (where the `Cargo.toml` file is found).
This step will add a complete starter project in place: `Cargo.toml` (already depending on `smplx-std`), `Simplex.toml`, `src/lib.rs` to let Rust code reference generated [artifacts](../glossary.md#artifacts), and starter contracts and tests.
Add or replace SimplicityHL source files in `simf/` as you go so Simplex can find them.
### Building artifacts
Run `simplex build` at the top-level directory of the project.
This process analyzes the SimplicityHL source code in `simf/` and creates corresponding Rust library files in `src/artifacts`, suitable for including from other Rust code. These library files define functions to build [witnesses](../glossary.md#witness) and blockchain [transactions](../glossary.md#transaction) to drive the individual SimplicityHL contract(s) within the Simplex project.
Whenever the SimplicityHL source code changes, re-run `simplex build` to regenerate the artifacts and keep them in sync with the contract's expectations.
???+ "SimplicityHL package dependencies"
If your contract's `.simf` import a separate SimplicityHL package (via `use` of an external crate, not just a local module), list it under `[dependencies]` in `Simplex.toml` and run `simplex install` to fetch it before building.
### Creating tests
Create `.rs` files in `tests/` containing your integration tests. Each such file defines a series of named integration tests as Rust functions, each with a prototype like
```rust
#[simplex::test]
fn test_name(context: simplex::TestContext) -> anyhow::Result<()> {
// Test code goes here.
Ok(())
}
```
### Running tests
Run `simplex test` to execute your complete integration test suite against a local `elementsd`. Adding `-v` will display verbose output from the tests.
Pass a name to run just one file's tests.
### Persistent regtest instance
`simplex test` normally spins up and tears down its own `elementsd`/Electrs pair for each run. Run `simplex regtest` in a separate terminal to instead start a standalone, long-lived node pair, then point `simplex test` at it via the `[test.rpc]`/`[test.esplora]` entries in `Simplex.toml`. This is useful when you want to inspect chain state between test runs instead of starting from scratch every time.
## Examples
[`BlockstreamResearch/smplx/examples`](https://github.com/BlockstreamResearch/smplx/tree/master/examples) contains sample Simplex projects demonstrating how to create integration tests.
The current examples are
* `examples/basic` (demonstrates a pay-to-public-key contract, called `p2pk.simf`)
Each has a `README.md` file describing the project and how to invoke its Simplex test suite.
You can also experiment with Simplex online in your browser using the [Simplicity Codespace](https://github.com/codespaces/new/blockstream/simplicity-codespace).
### Ecosystem & Wallet Integration
# Development ecosystem and wallet integration
A Simplicity contract enforces financial logic through on-chain transactions, but it is only one piece of a complete financial application. By itself, a contract lacks a user interface or discovery mechanism. To make a Simplicity contract useful, other software, such as a dedicated native app, a web app, or a wallet taught to interact with the contract, needs to be provided.
These supporting applications need to handle integration tasks such as finding active instances of a contract on the blockchain and validating that they have the intended or desired functionality, determining the current contract state and the actions a user can take, creating transactions that send or claim assets or that update a contract's state, and representing this information in a meaningful way for an end user.
Because Simplicity and Elements are open source, developers have the flexibility to create tools that facilitate these actions in a variety of development environments.
## Tooling (contract compilation and transaction generation)
### Command line
[Command-line tools for developing, testing, and exploration](../toolchain/) (`simc` and `hal-simplicity`) are available.
### SimplicityHL, rust-simplicity, and Simplex
The recommended development environment for building production applications that directly interact with Simplicity contracts is based on Rust. Three projects are most relevant to Rust developers.
* The [SimplicityHL](https://github.com/BlockstreamResearch/SimplicityHL) project includes a Rust library that lets you compile SimplicityHL to low-level Simplicity from inside a Rust program. The compilation logic is the same as in the `simc` compiler, but doesn't require a command-line invocation.
* The [`rust-simplicity`](https://github.com/BlockstreamResearch/rust-simplicity) library provides low-level functionality related to building witnesses and transactions. It can also derive on-chain addresses from a compiled Simplicity program.
* The [Simplex](../simplex/) orchestration tool can automatically generate code artifacts in Rust, compatible with `rust-simplicity`, that provide basic witness and transaction-building logic for a specified SimplicityHL program. It also provides other useful project management functionality such as dependency management and a test framework.
### LWK
The [Liquid Wallet Kit (LWK)](https://github.com/Blockstream/lwk/) provides higher-level functionality in Rust, with bindings also available for several other languages. Simplicity features are still being added in LWK as of April 2026, but are available as an alpha release behind the LWK compile-time feature `simplicity` (build with `cargo build --features simplicity`, `just build-bindings-lib-simplicity`, or `just python-build-bindings-simplicity`).
You can use LWK to build a complete wallet or blockchain app that interacts with Liquid Network, including interfacing with Simplicity contracts. LWK can also deal with storing digital assets' private keys directly in-app, and can generate addresses from a supplied seed. LWK's Simplicity support includes higher-level abstractions for transaction building for both commit and redeem transactions.
LWK can generate native apps or provide JavaScript bindings, with a WASM compile target, for building web apps.
A sample web app built in JavaScript using LWK's WASM target to generate Simplicity transactions is the [lending contract demo](https://demolending.distributedlab.com/). Its source code is available at [`BlockstreamResearch/simplicity-lending`](https://github.com/BlockstreamResearch/simplicity-lending).
## Tooling (blockchain indexing and discovery)
The main API for discovering and analyzing transactions on an Elements blockchain, including to determine smart contract state, is [Esplora](https://github.com/Blockstream/esplora). You can use Blockstream's public Esplora instance or run your own Esplora service.
If you'd like to run your own Liquid Network node, you can do so with a local instance of [Elements](https://github.com/ElementsProject/elements). The local storage footprint of a Liquid node, as of April 2026, is about 50 gigabytes.
## About transaction-building for interacting with Simplicity contracts
Sending an [asset](../glossary.md#asset) to a Simplicity [smart contract](../glossary.md#smart-contract) requires calculating the appropriate on-chain address for the contract, possibly including a [state commitment](./state.md) representing the contract's new state. This is called a *commit* transaction.
Determining the appropriate address for a commit transaction may involve moderately complex logic, including [Taproot](../glossary.md#taproot) details (creating a P2TR address where the Simplicity program and state are committed in the Taptree). If the contract does not require updated state commitments, the address of a particular instance of a particular contract will not change from transaction to transaction, and so can be hard-coded or provided by an external tool. The overall form of the commit transaction simply looks like any other P2TR transaction; Simplicity-specific transaction-building logic may not be required.
Claiming an asset from a UTXO controlled by a Simplicity contract is called a *redeem* transaction. It requires building a complete transaction with witness data, control block, and possibly appropriate signatures. Simplicity-specific transaction-building logic is usually required for such a transaction.
A [covenant](../glossary.md#covenant) transaction, in which assets controlled by a contract are sent back to a version of that same contract, generally includes *both* redeem and commit halves in the same transaction.
If an application needs signatures from an existing wallet, a [PSET](../glossary.md#pset) representing a redeem transaction can be built by Simplicity-aware software, and external software can produce an appropriate signature over the proposed PSET transaction. These signatures can be inserted into the [witness](../glossary.md#witness) witness data.
## Wallet connection mechanisms
An official wallet connection recommendation is currently in development to connect external wallets with Simplicity clients.
Work on this mechanism is built on top of the Bitcoin ecosystem's existing [WalletConnect protocol](https://walletconnect.com/), including a facility for pairing an external wallet with a web application via WebSockets. This permits an existing wallet, with minimal changes, to provide signatures for transactions with Simplicity smart contracts.
In this model, the web application (which is developed with LWK targeting WASM) provides the main user interface for a given smart contract, including visualizing contract state and initiating actions with the contract, but users' digital assets can be held in an existent external application or device.
The interface is general in order to enable interoperability with any third-party wallet that supports the appropriate wallet connect extensions. However, if the wallet does not provide UI support for a specific contract, the web application must be trusted to accurately represent the meaning and effects of the contract state and requested signatures.
A proof of concept of some wallet connect mechanisms is available in the [lending contract demo](https://lending.dev.blockstream.com/). Lightly-customized versions of Blockstream Jade, the Blockstream App, and other wallet applications can pair with this demo and authorize lending contract transactions on Liquid testnet, in both lender and borrower roles.
### txmanifest
# txmanifest
`txmanifest` is a JSON-based file format for describing Simplicity [contracts](../glossary.md#contract). It provides a reusable, standardized way to *teach wallets* how to describe and interact with a Simplicity contract on-chain. This reduces the need to build custom UI and transaction logic for each contract/wallet pair.
When a wallet application imports a trusted [manifest](../glossary.md#manifest) file describing a contract, the wallet application learns how to recognize instances of the described contract on the blockchain, how to describe those details to a user, and how to build new transactions that perform actions in that contract instance.
For smart contract developers, `txmanifest` provides a way to describe a contract once and achieve interoperability with a whole ecosystem of wallets.
For wallet developers, `txmanifest` provides a way to add UI and transaction functionality once and achieve interoperability with a whole ecosystem of smart contracts.
## Online documentation
The `txmanifest` format is work-in-progress and has not yet been frozen as a released specification. Detailed documentation for the `txmanifest` format is available in the [*txmanifest book*](https://stringhandler.github.io/tx_manifest_book/).
## Reference implementation
The [`txmanifest-wallet`](https://github.com/stringhandler/txmanifest-wallet/) project provides a reference implementation of `txmanifest` as a developer-oriented wallet application with a textual user interface.
You can use `txmanifest-wallet` to debug and experiment with manifest files describing new or existing Simplicity contracts. An interactive online demonstration of this tool, with sample manifests and contracts, is available in the [txw codespace](https://github.com/stringhandler/txw-codespace).
## Security considerations
The `txmanifest` format does not include a means to confirm or verify the correctness of descriptions.
Trusting an inaccurate or deceptive manifest file can result in wallets misinterpreting the meaning or effect of transactions, and misdescribing them to users. This could allow users' assets to be lost or stolen because the users approve transactions with undesired or unintended effects, including transferring assets to an attacker's control.
For example, a manifest file could falsely state that transferring currency to a certain address deposits it as collateral for a loan, which can purportedly be reclaimed by repaying the loan. In reality, the loan could have highly unfavorable terms that the wallet application fails to explain correctly, or the destination address could be some other form of contract that forfeits the user's deposit to an attacker with no further recourse.
Users and wallets must only import manifest files from appropriately trusted sources, and not automatically accept manifest files posted online or sent to them by strangers.
### Use Cases Overview
# Use Cases Overview
Simplicity can be used for a wide range of financial applications.
## Blockstream's reference implementations
Examples developed by Blockstream include
* [Simplicity DEX](./simplicity-dex/) for financial options
* [Simplicity Lending Protocol](./lending-protocol/) for collateralized lending ([implementation](https://github.com/BlockstreamResearch/simplicity-lending))
* [SHRINCS Simplicity verifier](https://github.com/BlockstreamResearch/shrincs-simplicity-verifier), for [Simplicity-based postquantum signature verification](https://blog.blockstream.com/blockstream-research-demonstrates-quantum-resistant-transaction-signing-on-liquid-using-simplicity-smart-contracts/) on Liquid (see also the [PQ Liquid Wallet](https://github.com/smeneguz/pq-liquid-wallet) hackathon project)
## Simplicity projects built by others
* [Resolvr](https://resolvr.io/) has created [Astrolabe](https://docs.simplicity-lang.org/news/2026/02/20/video-resolvr-astrolabe-demo/), a Simplicity-based reinsurance investment platform, and [Deadcat](https://github.com/Resolvr-io/deadcat), a Simplicity-based prediction market.
* [SideSwap](https://sideswap.io/) has [launched](https://sideswap.io/news/sideswap-press-release/) [Swaption](https://swaption.io/), a Simplicity-based noncustodial binary options marketplace.
* [StarkWare](https://starkware.co/) has used Simplicity to [implement a STARK verifier](https://starkware.co/blog/building-starks-in-simplicity/).
* [OceanSlim](https://github.com/0ceanslim) has created [anchor](https://github.com/0ceanSlim/anchor), a constant-product automated-market maker (AMM).
## Other examples and ideas
Check out the [SimplicityHL examples](https://github.com/BlockstreamResearch/SimplicityHL/tree/master/examples) for other introductory smart contract examples, including [vault](../glossary.md#vault) and [covenant](../glossary.md#covenant) mechanisms.
In addition to options, lending, insurance, and prediction markets, Simplicity is expected to support
* Cross-chain atomic swaps
* Crowdfunding contracts
* Bitcoin native smart contracts via Simplicity Unchained
* Vaults (multi-stage withdrawals)
### Simplicity DEX
# Simplicity DEX
The underlying code for this project is found in the [Simplicity DEX repository](https://github.com/Blockstream/simplicity-dex) (other relevant repository links appear below).
Despite the name, there is no exchange of Token A for Token B in the traditional sense at the core of the Simplicity DEX. It is a **structured product marketplace** that enables users to create and trade options contracts on-chain on the Liquid Network. Similar techniques can extend it to advertise and perform direct, immediate exchange of pairs of assets in a decentralized way without an intermediary; that functionality is planned for future development.
The current protocol facilitates *only* the exchange of "Grantor Tokens" plus a premium in USD for Liquid Bitcoin (LBTC) tokens. Support for other variations will be added in the future.
The existing code uses Nostr to publicize the existence of contracts and allow a party to locate a counterparty. This document focuses mainly on the financial logic of the contract rather than the technical mechanisms for representing the contract on Nostr and Liquid.
## Core definitions
**Call Option**: A financial contract that gives the holder the right, but not the obligation, to *buy* an underlying asset at a specified strike price before or at a specified expiration date.
**Put Option**: A financial contract that gives the holder the right, but not the obligation, to *sell* an underlying asset at a specified strike price before or at a specified expiration date.
> **Note**: The underlying [smart contract](../glossary.md#smart-contract) supports both Call and Put options, but the current CLI implementation only supports Call options.
**Grantor Token**: A tradable token received by the maker upon funding an options contract. The Grantor Token represents the right to claim assets at settlement - either the LBTC deposited during exercise (if the option is exercised) or the USDt collateral (if the option expires unexercised). In the current version of the protocol, the maker sells this token along with a premium in USD to a taker in exchange for LBTC.
## Implementation
The implemented protocol is a variation of a Call Option. The premium to the taker is paid during the exchange of LBTC and collateral tokens (i.e., Grantor Token).
### Participants
**Maker ("Option buyer")**: The party that creates the options contract. Upon creation, the maker:
* Deposits USDt collateral into the Collateral Covenant
* Receives both an **Option Token** and a **Grantor Token**
* Keeps the Option Token (gives the right to exercise)
* Pays a premium in USD to incentivize the taker to buy the Grantor Token for LBTC.
**Taker ("Option seller")**: The party that purchases the Grantor Token and receives the premium in USD. The taker:
* Pays LBTC to acquire the Grantor Token from the maker
* Holds the Grantor Token, which entitles them to claim assets at settlement
### Resources
The core contract of the Simplicity DEX is the Options contract:
The concept for this contract was proposed in the following whitepaper:
Link to the [Simplicity DEX repository](https://github.com/Blockstream/simplicity-dex).
## Financial Incentive
**Maker Profit Condition**: The maker profits when the value of the LBTC received (from selling the Grantor Token) exceeds the value of the USDt collateral deposited and the premium paid. This occurs when the LBTC price rises significantly above the strike price.
**Taker Profit Condition**: The taker profits when the premium they receive covers any potential losses, while still retaining a guaranteed claim on either LBTC (if the option is exercised) or the USDt collateral (if the option expires unexercised).
{ width="1457" height="888" }
!!! info "Option Offer Contract"
The [Option Offer contract](https://github.com/BlockstreamResearch/simplicity-contracts/blob/main/crates/contracts/src/finance/option_offer/source_simf/option_offer.simf) enables depositing two assets (collateral + premium) into a single covenant. A counterparty can then swap their settlement asset for both deposited assets in a single atomic transaction, with amounts enforced by configurable ratios (`collateral_per_contract` and `premium_per_collateral`).
## Detailed Step-by-Step Protocol Flow (Current implementation)
The maker selects locked-asset as USDt, and claim-asset as LBTC. The contract size is the collateral, e.g., 115,000 USDt. The strike price is $115,000 (LBTC/USDt). The term length is 30 days. The maker creates the "Options Creation" transaction which produces an Option Token Generator and a Grantor Token Generator held in a Generator Covenant.
The maker funds a single option using the "Option Funding" transaction, which requires depositing 115,000 USDt into the Collateral Covenant. The maker receives an Option Token and a Grantor Token from the Generator Covenant.
Note: at any time, the options contract can be canceled and the collateral retrieved by using both the Option Token and the Grantor Token together to unlock the collateral.
The maker deposits their newly minted Grantor Token along with premium (e.g., USDt) into the Option Offer contract. The taker pays LBTC (settlement asset) to the Option Offer contract and receives both the Grantor Token and the premium in a single atomic transaction. The maker keeps their Option Token.
**Case A: LBTC ends below $115,000.** Just prior to expiration, the maker uses their Option Token to exercise the Collateral Covenant. This requires depositing LBTC into a Settlement Covenant, which in turn unlocks the Collateral Covenant, whose 115,000 USDt funds the maker gets to keep. Afterwards, the taker can use their Grantor Token to retrieve the LBTC from the Settlement Covenant.
**Case B: LBTC ends above $115,000 or the maker lacks adequate LBTC funds.** The maker does nothing and their Option Token is useless. After the expiration date, the taker uses their Grantor Token to retrieve the 115,000 USDt from the Collateral Covenant.
### Key Design Properties
This contract design avoids the need for a price oracle, relying instead on the Option Token holder's (i.e., the maker's) natural incentives to choose the appropriate outcome. The taker is required to get the lesser-valued asset of the two, after the expiration date. The choices the maker can make can only improve the taker's outcome. The maker could incorrectly exercise or not exercise their option, which will cause the taker to end up with the more-valuable asset, or the maker could incorrectly exercise their option early, allowing the taker to get access to their funds prior to the expiration date, or both.
*It is the responsibility of the maker to optimize their handling of the options* in order to extract the most value for themselves. Any other outcome can only benefit the taker.
Avoiding the price oracle also automatically handles the case where the maker defaults: the taker gets the USDt collateral, regardless of the LBTC price.
The options design includes extra features beyond the basic specification. In particular, the Grantor and Option Tokens are real tradable assets, and both can be resold. This is particularly useful when the maker is in default (i.e., lacks sufficient funds to exercise the option) but the option is otherwise in-the-money: the maker can sell their Option Token to someone else who does have sufficient funds to exercise it.
The design also allows for more than one pair of Grantor/Option Tokens to be generated. For example, the maker can generate 10 token pairs along with 10 Collateral Covenant UTXOs, each holding one tenth of the total collateral. This enables partially filling the order. These sets of Option Tokens and Grantor Tokens are fungible and tradeable. Unfilled orders (i.e., when not all the Grantor Tokens are sold) can be used, along with the same number of Option Tokens, to cancel unused Collateral Covenants and recover the funds immediately.
```mermaid
sequenceDiagram
participant Maker
participant Options as Options Contract
participant OptionOffer as Option Offer Contract
participant Taker
Note over Maker,Taker: Phase 1 & 2: Creation and Funding
Maker->>Options: Create Options Contract
Options-->>Maker: Generator Covenant created
Maker->>Options: Deposit USDt collateral
Options-->>Maker: Option Token + Grantor Token
Note over Maker,Taker: Phase 3: Token Sale via Option Offer
Maker->>OptionOffer: Deposit Grantor Token + Premium (USDt)
Taker->>OptionOffer: Pay LBTC (settlement)
OptionOffer-->>Taker: Grantor Token + Premium
Maker->>OptionOffer: Withdraw settlement (LBTC)
OptionOffer-->>Maker: Repay full amount of settlement (LBTC)
Note over Maker,Taker: Phase 4a: Settlement (spot < strike)
alt LBTC price below strike
Maker->>Options: Exercise with Option Token + LBTC
Options-->>Maker: Release USDt collateral
Taker->>Options: Claim with Grantor Token
Options-->>Taker: Release LBTC from Settlement
end
Note over Maker,Taker: Phase 4b: Settlement (spot >= strike)
alt LBTC price above strike (or maker default)
Note over Maker: Option expires worthless
Taker->>Options: Claim with Grantor Token
Options-->>Taker: Release USDt collateral
end
```
## Appendix A: Excalidraw file
[profit-loss-diagram(2).excalidraw 28916](/assets/9e9ecabd-7792-4970-a69c-28847b5e9c99.excalidraw)
## Appendix B: Mapping to Traditional Options Positions
The Options contract is flexible enough to create all four standard options positions depending on two parameters: the collateral type and which token the maker sells.
However, the current contract configuration forces the premium to be of the collateral type, not in USD.
{ width="510" height="323" }
### The Two Determining Factors
**Collateral type determines Call vs Put:**
* **USDt collateral** → Call-like structure (the option is fundamentally about acquiring LBTC)
* **LBTC collateral** → Put-like structure (the option is fundamentally about acquiring USDt)
**Which token is sold determines who holds the Long vs Short position:**
* **Sell Grantor Token** (keep Option Token) → Maker holds the "long" position (has the right to exercise)
* **Sell Option Token** (keep Grantor Token) → Taker holds the "long" position (has the right to exercise)
### The Four Configurations
| Configuration | Collateral | Token Sold | Maker's Position | Taker's Position |
|----|----|----|----|----|
| 1 | USDt | Grantor | Long Call | Short Call |
| 2 | USDt | Option | Short Call | Long Call |
| 3 | LBTC | Grantor | Long Put | Short Put |
| 4 | LBTC | Option | Short Put | Long Put |
**Configuration 1 (USDt collateral, sell Grantor)** is the current implementation described in this document. The maker profits when LBTC rises significantly (Long Call profile), while the taker has limited upside but collects the premium spread (Short Call profile).
**Configuration 2 (USDt collateral, sell Option)** flips the positions. The taker, now holding the Option Token, has the right to exercise and profits when LBTC falls (Long Call on USDt, effectively). The maker keeps the Grantor Token and has the Short Call profile.
**Configuration 3 (LBTC collateral, sell Grantor)** creates a Put-like structure. The maker deposits LBTC as collateral. The Option Token holder can deposit USDt to claim the LBTC. The maker (holding Option Token) profits when LBTC falls (Long Put profile).
**Configuration 4 (LBTC collateral, sell Option)** is the inverse of Configuration 3. The taker holds the Option Token and has the Long Put position, profiting when LBTC falls. The maker has the Short Put profile.
### Practical Implications
This flexibility means market participants can use the same underlying contract to express different market views:
* **Bullish on LBTC**: Use Configuration 1 (maker) or Configuration 4 (taker)
* **Bearish on LBTC**: Use Configuration 3 (maker) or Configuration 2 (taker)
* **Neutral/yield-seeking**: Take the "short" side of any configuration to collect premium
The current CLI implementation supports Configuration 1, but the underlying smart contract is capable of supporting all four configurations.
### Lending Protocol
# Simplicity Lending Protocol on Liquid Network
[Simplicity](../glossary.md#simplicity) is a deterministic, statically analyzable smart contract language designed for Bitcoin-like systems, with predictable execution costs and a semantics that lends itself to high assurance reasoning and formal verification.
This article describes a Simplicity Lending Protocol implemented as Simplicity [smart contracts](../glossary.md#smart-contract) on the [Liquid Network](../glossary.md#liquid). The article focuses on the protocol design, financial logic, and contract decomposition. The Protocol implements on-chain borrowing at interest against collateral, with details agreed and accepted between two parties.
The Protocol can be used by any kind of entity, ranging from a financial institution to a private individual.
The Protocol handles accounting for a loan involving two different tokens, representing different [assets](../glossary.md#asset). For illustrative purposes, USDT is used as a loaned asset and LBTC as a collateral asset, but the protocol handles any pair of assets that are tokenized on Liquid.
The implementation plan consists of steps for the full Protocol implementation. It starts with verified lending primitives and increases the complexity and functionality of the Protocol over time.
The existing implementation can be found [on GitHub](https://github.com/BlockstreamResearch/simplicity-lending).
## Simplicity Lending Protocol Roadmap
### P2P Simplified Lending Contract
These are the parameters of the lending contract:
* **Borrower** - user with collateral asset A.
* **Lender** - user with loan asset B.
* **Collateral Amount** - pledged by Borrower collateral in asset A.
* **Loan Amount** - amount of asset B tokens borrowed by Borrower from Lender.
* **Lending Term** - moment in the future (UNIX time, block number, etc.) before which Borrower should repay the Loan Amount in asset B to the Lender.
* **Loan Amount** - the amount of the loan asset Borrower would like to borrow.
* **Collateral Amount** - locked by the Borrower, the collateral amount for the Loan Amount.
* **Liquidation** - if the Loan is not repaid after the Lending Term, the Lender can claim the collateral.
* **Loan Fee** - fixed fee paid in loan asset B from Borrower to Lender for the Loan after the loan is originated. Can represent interest payment for a fixed-term loan.
* **Origination Fee** - fixed fee paid in collateral asset A from Borrower to Lender for the Loan for loan origination.
* **Protocol Fee (Reserve Factor)** - fee paid in loan asset B to the Protocol Address for the Loan after the loan repayment. The fee is calculated as a percentage of the Loan Fee.
```mermaid
flowchart TD
START((Start))
S_Proposed[State: Collateral Locked]
S_Active[State: Loan Active]
T_Cancelled([Terminated: Refunded])
T_Settled([Settled: Repaid])
T_Defaulted([Liquidated: Defaulted])
START -->|Borrower proposes terms Borrower pays collateral| S_Proposed
S_Proposed -->|Borrower cancels Collateral refunded to borrower| T_Cancelled
S_Proposed -->|Lender accepts Lender pays principal Borrower receives principal| S_Active
S_Active -->|Loan repaid Borrower repays principal Principal repaid to lender Collateral refunded to borrower| T_Settled
S_Active -->|Timeout reached Liquidation process invoked Collateral paid to lender or liquidator| T_Defaulted
style S_Proposed fill:#fff4dd,stroke:#d4a017,stroke-width:2px
style S_Active fill:#fff4dd,stroke:#d4a017,stroke-width:2px
style T_Cancelled fill:#eee,stroke:#999
style T_Settled fill:#d1fae5,stroke:#10b981
style T_Defaulted fill:#fee2e2,stroke:#ef4444
```
#### Loan Origination
##### Borrower to Lender
Borrower locks Collateral Amount and the Origination Fee in the offer transaction. The Borrower also states the following parameters in the transaction: the Loan Fee, Lending Term, Origination Fee, and the Loan Amount the Borrower would like to borrow.
After an offer is published with a transaction, any address can accept the offer as a Lender.
##### Lender to Borrower
If the Lender is satisfied with the loan conditions in the offer (Origination Fee, Loan Fee, Protocol Fee, Lending Term, Loan Amount, Collateral Amount), the Lender accepts the offer and sends the Loan Amount, receives a fixed Origination Fee in a transaction, and accepts the conditions of the loan, including Origination Fee, Loan Fee, Protocol Fee, Lending Term and Collateral Amount.
The Borrower receives the requested Loan Amount in the offer.
#### Loan Repayment
The Borrower should fully repay the loan plus the Loan Fee at any moment before the Lending Term. Loan Fee is divided into the Protocol Fee paid to the Protocol Address and the rest paid to the Lender. After the repayment, Borrower can claim back the full amount of the collateral asset.
#### Liquidation
If the Borrower did not repay the full Loan Amount before the Lending Term, the Lender can claim the full collateral amount.
### P2P Lending Contract with Mock Price Oracle and Partial Loan Repayment
This contract variant is based upon the contract described in the prior section, with additional functionality and an additional Liquidation Loan-To-Value (LLTV) parameter.
Additional definitions:
* **Price Oracle** - source of constant verified price information of asset A in terms of asset B, signed by a trusted provider.
* **Third Party Liquidator** - any third party address that can fully repay the loan outstanding at the moment of liquidation and get the Collateral Amount.
* **Liquidation Protocol Fee** - fee paid to Protocol Address as a percentage (10%, for example) from the Collateral Amount at the liquidation event.
#### Loan Origination
##### Borrower to Lender
Borrower locks Collateral Amount and the Origination Fee in the transaction. The Borrower also states the transaction Loan Fee, Lending Term, Origination Fee, Loan Amount the Borrower would like to borrow, which should satisfy the following equation:
$$ \text{Loan Amount} < \text{LLTV} \times \text{Collateral Amount} \times \text{Price Oracle} $$
##### Lender to Borrower
If the Lender is satisfied with the loan conditions in the offer (Origination Fee, Loan Fee, Protocol Fee, Lending Term, Loan Amount, Collateral Amount, and LLTV), the Lender accepts the offer and sends a transaction with the Loan Amount in asset B and receives a fixed Origination Fee.
The Borrower receives the requested Loan Amount in the offer.
#### Loan Repayment
The Borrower should fully repay the Loan Amount plus Loan Fee at any moment before the expiration of the Lending Term. Loan Fee is divided into the Protocol Fee paid to the Protocol Address and the remainder paid to the Lender. After the repayment, Borrower can claim back the full amount of the collateral asset.
During the Lending Term, Borrower can choose a partial loan repayment to avoid a Liquidation event.
#### Liquidation
In case of a default on the loan (if the Borrower did not repay the full Loan Amount plus the Loan Fee before the Lending Term), the Lender can claim the full collateral amount. (But see below for a partial liquidation alternative.)
Liquidation at any moment (before or after Lending Term) by a Third Party Liquidator is possible if the following equation is true:
$$ \text{Loan Amount Outstanding} > \text{LLTV} \times \text{Collateral Amount} \times \text{Price Oracle} $$
In this case, Third Party Liquidator receives the Collateral Amount minus the Liquidation Protocol Fee, which goes to the Protocol Address.
#### Partial Liquidation Invariant
The full collateral liquidation option allows the Lender, after the Lending Term, to liquidate the full amount of collateral even if the loan is significantly repaid. To mitigate this case and give the Borrower partial credit for partial repayment, a partial liquidation invariant is available. This alternative pays the Lender a pro-rated fraction of the collateral in exchange for the outstanding portion of the loan principal. The contract also sets a fixed Liquidation Penalty as an additional amount of collateral obtained by the Lender as a penalty for incomplete repayment. The rest of the collateral is sent back to the Borrower.
In this case, liquidation after the Lending Term can be initiated by the Lender or the Borrower. The collateral part received by the Borrower is calculated according to the following equation:
$$ \text{Borrower Collateral Payment} = \text{Collateral Amount} - \frac{\text{Loan Amount Outstanding}}{\text{Price Oracle}} - \text{Liquidation Penalty} $$
Liquidation Penalty is also subject to Liquidation Protocol Fee, which goes to the Protocol Address.
### P2P Lending Contract with Integrated Interest Rate
This contract variant is based upon the contract described in the prior section, but, instead of a fixed Loan Fee, a fixed Interest Rate is used.
Additional definitions:
* **Interest Rate** - fixed annual percentage rate (APR), which is a reward for the Lender from the Borrower as an incentive to provide the loan.
#### Loan Origination
##### Borrower to Lender
Borrower locks Collateral Amount and the Origination Fee in the transaction. The Borrower also states in the transaction the Interest Rate, Lending Term, Origination Fee, Loan Amount the Borrower would like to borrow, which should satisfy the following equation:
$$ \text{Loan Amount} < \text{LLTV} \times \text{Collateral Amount} \times \text{Price Oracle} $$
If the Lender is satisfied with the loan conditions in the offer (Origination Fee, Interest Rate, Protocol Fee, Lending Term, Loan Amount, Collateral Amount), the Lender accepts the offer and sends a transaction with the loan in asset B and receives a fixed Origination Fee.
##### Lender to Borrower
Lender sends the Loan Amount in a transaction and accepts the conditions of the loan, including Origination Fee, Interest Rate, Protocol Fee, Lending Term, Collateral Amount and LLTV.
The Borrower receives the requested Loan Amount in the offer.
#### Loan Repayment
The Borrower should fully repay the Loan Amount with the accrued Interest Rate at any moment before the Lending Term. Loan Fee is divided into the Protocol Fee paid to the Protocol Address and the rest paid to the Lender. After the repayment Borrower can claim back the full amount of the collateral asset.
During the Lending Term Borrower can choose a partial loan repayment to avoid a Liquidation event.
#### Liquidation
If the Borrower did not repay the full Loan Amount plus the accrued Interest Rate before the Lending Term, the Lender can claim the full collateral amount.
Liquidation at any moment (before or after Lending Term) by the Third Party Liquidator is possible provided that the following equation is true:
$$ \text{Loan Amount Outstanding} + \text{Accrued Interest Rate} > \text{LLTV} \times \text{Collateral Amount} \times \text{Price Oracle} $$
Third Party Liquidator receives the Collateral Amount minus the Liquidation Protocol Fee, which goes to the Protocol Address.
#### Partial Liquidation Invariant
The full collateral liquidation option allows the Lender, after the Lending Term, to liquidate the full amount of collateral even if the loan is significantly repaid. To mitigate this case, a partial liquidation mechanism is available. The partial liquidation invariant sets the Liquidation Penalty as a fixed collateral asset amount obtained by the Lender on top of the recovered loan amount. The rest of the collateral is sent back to the Borrower.
In this case, liquidation after the Lending Term can be initiated by the Lender or the Borrower. The collateral part received by Borrower is calculated according to the following equation:
$$ \text{Borrower Collateral Payment} = \text{Collateral Amount} - \frac{\text{Loan Amount Outstanding}}{\text{Price Oracle}} - \text{Liquidation Penalty} $$
Liquidation Penalty is also subject to Liquidation Protocol Fee, which goes to the Protocol Address.
## First Lending Protocol Implementation
The first version of the Protocol is being finalized with a P2P Simplified Lending Contract solution, which will be deployed on the Liquid Network and allow lending USDT against LBTC for a fixed term.
The first lending protocol implementation is carried out by multiple [covenants](../glossary.md#covenant) that are constructed continuously by both Borrower and Lender in the following order:
### Creation of Utility Tokens
Both Lender's and Borrower's financial interests in the contract are represented by special "one-time" assets ([tokens](../glossary.md#token)) that enable the existence of secondary markets where these tokens can be sold/traded/exchanged.
To ensure the integrity of lending offer parameters across multiple covenants, specialized Parameter Tokens are utilized. These assets act as data carriers by encoding the protocol parameters directly within their amount fields.
**Important:** To ensure compatibility with Liquid/Elements consensus rules, the encoded value must not exceed the maximum allowed supply limit ([`MAX_MONEY`](https://github.com/ElementsProject/rust-elements/blob/f6ffc7800df14b81c0f5ae1c94368a78b99612b9/src/blind.rs#L471)). This effectively restricts the available data space within the token's amount field to 51 bits.
Two distinct tokens are required to store the current protocol state:
1. `FIRST_PARAMETERS_NFT`: Encodes the `PRINCIPAL_INTEREST_RATE`, `LOAN_DURATION`, `COLLATERAL_DECIMALS_MANTISSA`, and `PRINCIPAL_DECIMALS_MANTISSA`.
| Bits | Field name | Description |
| :---- | :---- | :---- |
| \[0..15\] | Interest Rate | 16-bit value for the loan interest (percentage) |
| \[16..42\] | Loan Expiration Time | 27-bit value for the loan expiration time in blocks (block height) |
| \[43..46\] | Collateral Decimals Mantissa | 4-bit exponent for collateral amount calculation |
| \[47..50\] | Principal Decimals Mantissa | 4-bit exponent for principal amount calculation |
2. `SECOND_PARAMETERS_NFT`: Encodes the `BASE_COLLATERAL_AMOUNT` and `BASE_PRINCIPAL_AMOUNT`.
| Bits | Field name | Description |
| :---- | :---- | :---- |
| \[0..24\] | Base Collateral Amount | 25-bit integer representing the collateral amount without decimals |
| \[25..49\] | Base Principal Amount | 25-bit integer representing the principal amount without decimals |
| \[50\] | Free Space | - |
Since the amount in a single [UTXO](../glossary.md#utxo) cannot exceed $21,000,000 \times 10^8$, the number of bits required to encode the parameters can be reduced:
- Decimals mantissa (4 bits): This field occupies only 4 bits because assets with more than 8 decimals can't be created on Liquid.
- Base amount (25 bits): The base amount occupies 25 bits, allowing for a maximum value of 33,554,432. When combined with the decimals mantissa, it can represent any value from 0 up to the maximum possible supply.
- Interest Rate (16 bits): This field requires a minimum of 14 bits to express percentages using a scale where 10,000 equals 100%. Using 16 bits achieves a maximum interest rate of 655.36%.
- Loan expiration time in blocks: Setting this value to 27 bits allows the end block of the loan to be 134,217,728. This is currently ~248 years in the future with a 1-minute block rate.
### Lock Collateral
After calculating the Utility asset IDs, the "lock collateral" covenant can be constructed.
### Set up Lending
After the Borrower has constructed a "lock collateral" covenant, a Lender can then initiate the loan by sending the lending covenant transaction.
### Settle Lending
After the lending contract is set up, there are two ways it can be settled. Either the Borrower provides the principal with interest to the Lender and takes the collateral back, or the Lending Term expires, and the Lender liquidates the position, claiming the collateral.
## Indexing and Discovery Mechanism
The indexing mechanism is a critical component of the protocol's UX. To make the lending workflow functional, Lenders must be able to view all active loan offers from Borrowers. Simultaneously, Borrowers require real-time updates on the status of their offers to monitor repayment deadlines and obligations. The following indexing strategy is used to aggregate and display this data.
### Pre-lock Transaction Discovery and Verification
To monitor all active orders, an indexer scans the blockchain to identify transactions that initialize Pre-lock covenants. Once these transactions are identified, an indexer monitors the lifecycle of the resulting UTXOs to extract and update information regarding the loan terms.
An indexer recognizes Pre-lock initialization transactions by matching them against a specific transaction template:
- The transaction must contain a minimum of 5 inputs and 6 outputs.
- The transaction must include an `OP_RETURN` output containing a valid [BIP340](https://en.bitcoin.it/wiki/BIP_0340) public key.
- The first and second inputs must contain Parameter UTXOs, where the encoded amounts match the bit-packed lending protocol structure.
If a transaction meets the criteria above, the indexer performs a verification:
1. It attempts to compile the Pre-lock covenant using the parameters extracted from the transaction data.
2. It compares the resulting script hash ([CMR](../glossary.md#cmr)) with the script hash of output 0.
3. If the hashes match, the transaction is confirmed as a valid Pre-lock initialization.
### State Management
An indexer maintains a database of all existing offers. It continuously monitors the blockchain for spending transactions that target protocol-controlled outputs, allowing it to update the status of each loan (e.g., Active, Repaid, Cancelled, or Liquidated) in near real-time.
## Reference
### Introduction
# Introduction
SimplicityHL is a high-level language for writing Bitcoin smart contracts.
In other words, SimplicityHL is a language for expressing spending conditions of [UTXOs](../glossary.md#utxo) on the Bitcoin blockchain.
SimplicityHL looks and feels like [Rust](https://www.rust-lang.org/).
Developers write SimplicityHL, Bitcoin full nodes run Simplicity.
# Types and Values
- [Types and Values](./type.md)
- [Type Aliases](./type_alias.md)
- [Type Casting](./type_casting.md)
# Writing a Program
- [Let Statements](./let_statement.md)
- [Match Expression](./match_expression.md)
- [Functions](./function.md)
- [Programs](./program.md)
### Types and Values
# Types and Values
SimplicityHL mostly uses a subset of Rust's types.
It extends Rust in some ways to make it work better with Simplicity and with the blockchain.
## Boolean Type
| Type | Description | Values |
|--------|-----------------|-----------------|
| `bool` | Boolean | `false`, `true` |
Values of type `bool` are truth values, which are either `true` or `false`.
## Integer Types
| Type | Description | Values |
|--------|-----------------|--------------------------------------------------------|
| `u1` | 1-bit integer | `0`, `1` |
| `u2` | 2-bit integer | `0`, `1`, `2`, `3` |
| `u4` | 4-bit integer | `0`, `1`, …, `15` |
| `u8` | 8-bit integer | `0`, `1`, …, `255` |
| `u16` | 16-bit integer | `0`, `1`, …, `65535` |
| `u32` | 32-bit integer | `0`, `1`, …, `4294967295` |
| `u64` | 64-bit integer | `0`, `1`, …, `18446744073709551615` |
| `u128` | 128-bit integer | `0`, `1`, …, `340282366920938463463374607431768211455` |
| `u256` | 256-bit integer | `0`, `1`, …, 2256 - 1 |
Unsigned integers range from 1 bit to 256 bits.
[`u8`](https://doc.rust-lang.org/std/primitive.u8.html) to [`u128`](https://doc.rust-lang.org/std/primitive.u128.html) are also supported in Rust.
`u1`, `u2`, `u4` and `u256` are new to SimplicityHL.
Integer values can be written in decimal notation `123456`, binary notation `0b10101010` or hexadecimal notation `0xdeadbeef`. The number of bits or hex digits (for binary or hex notation) must correspond to the bit width of the type.
There are no signed integers.
The maximal `u256` value is `115792089237316195423570985008687907853269984665640564039457584007913129639935`.
## Tuple Types
| Type | Description | Values |
|--------------|-------------|---------------------------------------------------|
| `()` | 0-tuple | `()` |
| `(A)` | 1-tuple | `(a0,)`, `(a1,)`, … |
| `(A, B)` | 2-tuple | `(a0, b0)`, `(a1, b1)`, `(a2, b2)`, `(a3, b3)`, … |
| … | … | … |
| `(A, B, …)` | n-tuple | `(a0, b0, …)`, … |
[Tuples work just like in Rust](https://doc.rust-lang.org/std/primitive.tuple.html).
The empty tuple `()` contains no information.
It is also called the "unit".
It is mostly used as the return type of functions that don't return anything.
Singletons `(a0,)` must be written with an extra comma `,` to differentiate them from function calls.
Bigger tuples `(a0, b0, …)` work like in most other programming languages.
Each tuple type `(A1, A2, …, AN)` defines a sequence `A1`, `A2`, …, `AN` of types.
Values of that type must mirror the sequence of types:
A tuple value `(a1, a2, …, aN)` consists of a sequence `a1`, `a2`, …, `aN` of values, where `a1` is of type `A1`, `a2` is of type `A2`, and so on.
Tuples are always finite in length.
> Tuples are different from arrays:
> Each element of a tuple can have a different type.
> Each element of an array must have the same type.
## Array Types
| Type | Description | Values |
|----------|-------------|---------------------------------------------------|
| `[A; 0]` | 0-array | `[]` |
| `[A; 1]` | 1-array | `[a0]`, `[a1]`, … |
| `[A; 2]` | 2-array | `[a0, a1]`, `[a2, a3]`, `[a4, a5]`, `[a6, a7]`, … |
| … | … | … |
| `[A; N]` | n-array | `[a0, …, aN]`, … |
[Arrays work just like in Rust](https://doc.rust-lang.org/std/primitive.array.html).
The empty array `[]` is basically useless, but is included for completeness.
Arrays `[a0, …, aN]` work like in most other programming languages.
Each array type `[A; N]` defines an element type `A` and a length `N`.
An array value `[a0, …, aN]` of that type consists of `N` many elements `a0`, …, `aN` that are each of type `A`.
Arrays are always of finite length.
> Arrays are different from tuples:
> Each element of an array must have the same type.
> Each element of a tuple can have a different type.
## List Types
**SimplicityHL List types work differently from what you might expect from other languages. Please see their details and limitations below.** Notably, a List type must be declared with a length that is an exact power of two, and the List can never hold the full number of items indicated by its declared length.
| Type | Description | Values |
|---------------------------|---------------------|------------------------------------------------------|
| `List` | <2-list | `list![]`, `list![a1]` |
| `List` | <4-list | `list![]`, …, `list![a1, a2, a3]` |
| `List` | <8-list | `list![]`, …, `list![a1, …, a7]` |
| `List` | <16-list | `list![]`, …, `list![a1, …, a15]` |
| `List` | <32-list | `list![]`, …, `list![a1, …, a31]` |
| `List` | <64-list | `list![]`, …, `list![a1, …, a62]` |
| `List` | <128-list | `list![]`, …, `list![a1, …, a127]` |
| `List` | <256-list | `list![]`, …, `list![a1, …, a255]` |
| `List` | <512-list | `list![]`, …, `list![a1, …, a511]` |
| … | … | … |
| `List` | <2^N-list | `list![]`, …, `list![a1, …, a_{2^N - 1}]` |
Lists hold a variable number of elements of the same type.
This is similar to [Rust vectors](https://doc.rust-lang.org/std/vec/struct.Vec.html), but SimplicityHL doesn't have a heap.
In SimplicityHL, lists exist on the stack, which is why the maximum list length is bounded.
<2-lists hold fewer than 2 elements, so zero or one element.
<4-lists hold fewer than 4 elements, so zero to three elements.
<8-lists hold fewer than 8 elements, so zero to seven elements.
And so on.
For technical reasons, the list bound is always a power of two.
The bound 1 is not supported, because it would only allow empty lists, which is useless.
> Lists are different from arrays:
> List values hold a variable number of elements.
> Array values hold a fixed number of elements.
On the blockchain, you pay for every byte that you use.
If you use an array, then you pay for every single element.
For example, values of type `[u8; 512]` cost roughly as much as 512 individual `u8` values.
However, if you use a list, then you only pay for the elements that you actually use.
For example, the type `List` allows for up to 511 elements.
If you only use three elements `list![1, 2, 3]`, then you pay for exactly three elements.
You **don't** pay for the remaining 508 unused elements.
## Option Types
| Type | Values |
|-------------|-----------------------------------|
| `Option` | `None`, `Some(a0)`, `Some(a1)`, … |
Options represent values that might not be present. [They work just like in Rust](https://doc.rust-lang.org/std/option/index.html).
An option type is generic over a type `A`.
The value `None` is empty.
The value `Some(a)` contains an inner value `a` of type `A`.
In Rust, we implement options as follows.
```rust
enum Option {
None,
Some(A),
}
```
## Either Types
| Type | Values |
|----------------|--------------------------------------------------------|
| `Either` | `Left(a0)`, `Left(a1)`, …, `Right(b0)`, `Right(b1)`, … |
Sum types represent values that are of some "left" type in some cases and that are of another "right" type in other cases.
[They work just like in the either crate](https://docs.rs/either/latest/either/enum.Either.html).
[The Result type from Rust is very similar, too](https://doc.rust-lang.org/std/result/index.html).
A sum type is generic over two types, `A` and `B`.
The value `Left(a)` contains an inner value `a` of type `A`.
The value `Right(b)` contains an inner value `b` of type `B`.
In Rust, we implement sum types as follows.
```rust
enum Either {
Left(A),
Right(B),
}
```
## Enum Types
Enum types are available since SimplicityHL 0.7.0 (compiling with `-Z enums`).
Enum types are declared with a stanza like
```rust
enum A {
B,
C,
D,
E,
}
```
This declaration allows the type `A` to take on any of four listed values, called `A::B`, `A::C`, `A::D`, and `A::E`. The declaration stanza belongs at the top level of the program, outside of any executable code block.
Enum type values can also wrap objects of other types, when this is appropriately declared in the initial enum declaration. The `Enum` instances then contain inner variables of the specified types.
```rust
enum A {
B,
C(T),
D,
E(U),
}
```
In this declaration, `A::C` wraps another object of type `T`, while `A::E` wraps another object of type `U`.
Enum types are intended for use in matching witness values with the `match` statement, so that one of a list of pre-specified actions or choices can be selected easily. By wrapping other values where needed, the additional values appropriate to a specific action or choice can also be provided in the witness.
| Type | Values (pursuant to declaration of a specific Enum) |
|----------------|--------------------------------------------------------|
| `EnumX` | `EnumX::A(T)`, `EnumX::B(U)`, `EnumX::C(V)` …, |
### Type Aliases
# Type Aliases
SimplicityHL currently doesn't support Rust-like `struct`s for organizing data.
```rust
struct User {
active: bool,
id: u256,
sign_in_count: u64,
}
```
SimplicityHL programmers have to handle long tuples of unlabeled data, which can get messy.
```rust
(bool, u256, u64)
```
To help with the situation, programmers can define custom type aliases.
Aliases define a new name for an existing type.
In contrast, `struct`s define an entirely new type, so aliases are different from `struct`s.
However, aliases still make the code more readable.
```rust
type User = (bool, u256, u64);
```
There is also a list of builtin type aliases.
These aliases can be used without defining them.
| Builtin Alias | Definition |
|------------------|-------------------------------|
| `Amount1` | `Either<(u1, u256), u64>` |
| `Asset1` | `Either<(u1, u256), u256>` |
| `Confidential1` | `(u1, u256)` |
| `Ctx8` | `(List, (u64, u256))` |
| `Distance` | `u16` |
| `Duration` | `u16` |
| `ExplicitAmount` | `u256` |
| `ExplicitAsset` | `u256` |
| `ExplicitNonce` | `u256` |
| `Fe` | `u256` |
| `Ge` | `(u256, u256)` |
| `Gej` | `((u256, u256), u256)` |
| `Height` | `u32` |
| `Lock` | `u32` |
| `Message` | `u256` |
| `Message64` | `[u8; 64]` |
| `Nonce` | `Either<(u1, u256), u256>` |
| `Outpoint` | `(u256, u32)` |
| `Point` | `(u1, u256)` |
| `Pubkey` | `u256` |
| `Scalar` | `u256` |
| `Signature` | `[u8; 64]` |
| `Time` | `u32` |
| `TokenAmount1` | `Either<(u1, u256), u64>` |
### Type Casting
# Casting
A SimplicityHL type can be cast into another SimplicityHL type if both types share the same structure.
The structure of a type has to do with how the type is implemented on the Simplicity "processor".
Below is a table of types that can be cast into each other.
| Type | Casts To (And Back) |
|----------------|------------------------------------|
| `bool` | `Either<(), ()>` |
| `Option` | `Either<(), A>` |
| `u1` | `bool` |
| `u2` | `(u1, u1)` |
| `u4` | `(u2, u2)` |
| `u8` | `(u4, u4)` |
| `u16` | `(u8, u8)` |
| `u32` | `(u16, u16)` |
| `u64` | `(u32, u32)` |
| `u128` | `(u64, u64)` |
| `u256` | `(u128, u128)` |
| `(A)` | `A` |
| `(A, B, C)` | `(A, (B, C))` |
| `(A, B, C, D)` | `((A, B), (C, D))` |
| … | … |
| `[A; 0]` | `()` |
| `[A; 1]` | `A` |
| `[A; 2]` | `(A, A)` |
| `[A; 3]` | `(A, (A, A))` |
| `[A; 4]` | `((A, A), (A, A))` |
| … | … |
| `List` | `Option` |
| `List` | `(Option<[A; 2]>, List)` |
| `List` | `(Option<[A; 4]>, List)` |
| `List` | `(Option<[A; 8]>, List)` |
| `List` | `(Option<[A; 16]>, List)` |
| `List` | `(Option<[A; 32]>, List)` |
| `List` | `(Option<[A; 64]>, List)` |
| `List` | `(Option<[A; 128]>, List)` |
| `List` | `(Option<[A; 256]>, List)` |
| … | … |
## Casting Rules
Type `A` can be cast into itself (reflexivity).
If type `A` can be cast into type `B`, then type `B` can be cast into type `A` (symmetry).
If type `A` can be cast into type `B` and type `B` can be cast into type `C`, then type `A` can be cast into type `C` (transitivity).
## Casting Expression
All casting in SimplicityHL happens explicitly through a casting expression.
```rust
::into(input)
```
The above expression casts the value `input` of type `Input` into some output type.
The input type of the cast is explicit while the output type is implicit.
In SimplicityHL, the output type of every expression is known.
```rust
let x: u32 = 1;
```
In the above example, the meaning of the expression `1` is clear because of the type `u32` of variable `x`.
Here, `1` means a string of 31 zeroes and 1 one.
_In other contexts, `1` could mean something different, like a string of 255 zeroes and 1 one._
The SimplicityHL compiler knows the type of the outermost expression, and it tries to infer the types of inner expressions based on that.
When it comes to casting expressions, the compiler has no idea about the input type of the cast.
The programmer needs to supply this information by annotating the cast with its input type.
```rust
let x: u32 = <(u16, u16)>::into((0, 1));
```
In the above example, the tuple `(0, 1)` of type `(u16, u16)` is cast into type `u32`.
Consult the table above to verify this is a valid cast.
### Let Statements
# Let Statement
Variables are defined in let statements, [just like in Rust](https://doc.rust-lang.org/std/keyword.let.html).
```rust
let x: u32 = 1;
```
The above let statement defines a variable called `x`.
The variable is of type `u32` and it is assigned the value `1`.
```rust
let x: u32 = f(1337);
```
Variables can be assigned to the output value of any expression, such as function calls.
## Explicit typing
In SimplicityHL, the type of a defined variable **always** has to be written.
This is different from Rust, which has better type inference.
## Immutability
SimplicityHL variables are **always** immutable.
There are no mutable variables.
## Redefinition and scoping
The same variable can be defined twice in the same scope.
The later definition overrides the earlier definition.
```rust
let x: u32 = 1;
let x: u32 = 2;
assert!(jet::eq_32(x, 2)); // x == 2
```
Normal scoping rules apply:
Variables from outer scopes are available inside inner scopes.
A variable defined in an inner scope shadows a variable of the same name from an outer scope.
```rust
let x: u32 = 1;
let y: u32 = 2;
let z: u32 = {
let x: u32 = 3;
assert!(jet::eq_32(y, 2)); // y == 2
x
};
assert!(jet::eq_32(z, 3)); // z == 3
```
## Pattern matching
There is limited pattern matching support inside let statements.
```rust
let (x, y, _): (u8, u16, u32) = (1, 2, 3);
let [x, _, z]: [u32; 3] = [1, 2, 3];
```
In the first line, the tuple `(1, 2, 3)` is deconstructed into the values `1`, `2` and `3`.
These values are assigned to the variable names `x`, `y` and `_`.
The variable name `_` is a special name that ignores its value.
In the end, two variables are created: `x: u32 = 1` and `y: u16 = 2`.
Similarly, arrays can be deconstructed element by element and assigned to a variable each.
### Match Expression
# Match Expression
A match expression conditionally executes code branches.
Which branch is executed depends on the input to the match expression.
```rust
let result: u32 = match f(42) {
Left(x: u32) => x,
Right(x: u16) => jet::left_pad_low_16_32(x),
};
```
In the above example, the output of the function call `f(42)` is matched.
`f` returns an output of type `Either`.
If `f(42)` returns a value that matches the pattern `Left(x: u32)`, then the first match arm is executed.
This arm simply returns the value `x`.
Alternatively, if `f(42)` returns a value that matches the pattern `Right(x: u16)`, then the second match arm is executed.
This arm extends the 16-bit number `x` to a 32-bit number by padding its left with zeroes.
Because of type constraints, the output of `f` must match one of these two patterns.
The whole match expression returns a value of type `u32`, from one of the two arms.
## Explicit typing
In SimplicityHL, the type of variables inside match arms must **always** be written.
This is different from Rust, which has better type inference.
## Pattern matching
There is limited support for pattern matching inside match expressions.
Boolean values can be matched.
The Boolean match expression is the replacement for an "if-then-else" in SimplicityHL.
```rust
let bit_flip: bool = match false {
false => true,
true => false,
};
```
Optional values can be matched.
The `Some` arm introduces a variable which must be explicitly typed.
```rust
let unwrap_or_default: u32 = match Some(42) {
None => 0,
Some(x: u32) => x,
};
```
Finally, `Either` values can be matched.
Again, variables that are introduced in match arms must be explicitly typed.
```rust
let map_either: u32 = match Left(1337) {
Left(x: u32) => f(x),
Right(y: u32) => f(y),
};
```
Since SimplicityHL 0.5.0, the match expression also supports further pattern matching, similar to Rust.
```rust
let unwrap_or_default: u32 = match Some((4, 2)) {
None => 0,
Some((y, z): (u16, u16)) => <(u16, u16)>::into((y, z)),
};
```
This is more concise than including code to perform the deconstruction inside the match arm.
For example, the code above is a more concise alternative to this version that subsequently deconstructes the tuple `x` of type `(u16, u16)` into two integers `y` and `z` of type `u16`.
```rust
let unwrap_or_default: u32 = match Some((4, 2)) {
None => 0,
Some(x: (u16, u16)) => {
let (y, z): (u16, u16) = x;
<(u16, u16)>::into((y, z))
}
};
```
The match arm can also contain match expressions for further deconstruction.
For example, the sum value `x` of type `Either` can be matched as either `Left(y: u32)` or `Right(z: u32)`.
```rust
let unwrap_or_default: u32 = match Some(Left(42)) {
None => 0,
Some(x: Either) => match x {
Left(y: u32) => y,
Right(z: u32) => z,
},
};
```
## Enum matching
Since SimplicityHL 0.7.0 (compiling with `-Z enums`), you can match on all values of an `enum` type.
This is useful for *actions* at the top level of a contract's `main()` function. Matching an `enum` lets a [witness](../glossary.md#witness) choose from among several predefined actions.
```rust
enum Action {
Inherit(Signature),
ColdSpend(Signature),
HotSpend(Signature),
}
// ...
match witness::ACTION {
Action::Inherit(sig: Signature) => inherit_spend(sig),
Action::ColdSpend(sig: Signature) => cold_spend(sig),
Action::HotSpend(sig: Signature) => refresh_spend(sig),
}
```
In this example, `witness::ACTION` is an object of type `Action` (with the explicit value either `Inherit(sig)`, `ColdSpend(sig)`, or `HotSpend(sig)`). The SimplicityHL program uses this value to learn which action the proposed transaction is attempting to take, and call the appropriate function to validate the conditions for that action.
## Only two branches per `match`
Differently from Rust, the `match` expression only allows two match arms (branches) per `match` (except for `enum` types).
For example, it is currently not possible to match `Left(Left(x: u8))`, `Left(Right(x: u8)`, `Right(Left(x: u8))`, and `Right(Right(x: u8))` as arms of a single `match` expression.
Matching these four possibilities instead requires two nested `match` expressions.
This limitation does not apply to the `enum` matching described above. A `match` on an `enum` type should contain exactly one branch for each declared value of that `enum` type, regardless of how many values the `enum` type can take on.
### Defining Functions
# Functions
Functions are defined and called [just like in Rust](https://doc.rust-lang.org/std/keyword.fn.html).
```rust
fn add(x: u32, y: u32) -> u32 {
let (carry, sum): (bool, u32) = jet::add_32(x, y);
match carry {
true => panic!(), // overflow
false => {}, // ok
};
sum
}
```
The above example defines a function called `add` that takes two parameters: variable `x` of type `u32` and variable `y` of type `u32`. The function returns a value of type `u32`.
The body of the function is a block expression `{ ... }` that is executed from top to bottom.
The function returns on the final line _(note the missing semicolon `;`)_.
In the above example, `x` and `y` are added via the `add_32` jet.
The function then checks if the carry is true, signaling an overflow, in which case it panics.
On the last line, the value of `sum` is returned.
The above function is called by writing its name `add` followed by a list of arguments `(40, 2)`.
Each parameter needs an argument, so the list of arguments is as long as the list of parameters.
Here, `x` is assigned the value `40` and `y` is assigned the value `2`.
```rust
let z: u32 = add(40, 2);
```
## No early returns
SimplicityHL has no support for an early return via a "return" keyword.
The only branching that is available is via [match expressions](./match_expression.md).
## No recursion
SimplicityHL has no support for recursive function calls.
A function can be called inside a function body if it has been defined before.
This means that a function cannot call itself.
Loops, where `f` calls `g` and `g` calls `f`, are also impossible.
What _is_ possible are stratified function definitions, where level-0 functions depend on nothing, level-1 functions depend on level-0 functions, and so on.
```rust
fn level_0() -> u32 {
0
}
fn level_1() -> u32 {
let (_, next) = jet::increment_32(level_0());
next
}
fn level_2() -> u32 {
let (_, next) = jet::increment_32(level_1());
next
}
```
## Order matters
If function `g` calls function `f`, then `f` **must** be defined before `g`.
```rust
fn f() -> u32 {
42
}
fn g() -> u32 {
f()
}
```
## Main function
The `main` function is the entry point of each SimplicityHL program.
Running a program means running its `main` function.
Other functions are called from the `main` function.
```rust
fn main() {
// ...
}
```
The `main` function is a reserved name and must exist in every program.
SimplicityHL programs are always "binaries".
There is no support for "libraries".
## Jets
Jets are predefined and optimized functions for common use cases.
```rust
jet::add_32(40, 2)
```
Jets live inside the namespace `jet`, which is why they are prefixed with `jet::`.
They can be called without defining them manually.
It is usually more efficient to call a jet than to manually compute a value.
[The jet documentation](https://docs.rs/simfony-as-rust/latest/simfony_as_rust/jet/index.html) lists each jet and explains what it does.
### Programs
# Programs
A SimplicityHL program consists of a `main` [function](./function.md).
A program may also have [type aliases](./type_alias.md) or custom [function definitions](./function.md).
The `main` function comes last in the program, because everything it calls must be defined before it.
```rust
type Furlong = u32;
type Mile = u32;
fn to_miles(distance: Either) -> Mile {
match distance {
Left(furlongs: Furlong) => jet::divide_32(furlongs, 8),
Right(miles: Mile) => miles,
}
}
fn main() {
let eight_furlongs: Either = Left(8);
let one_mile: Either = Right(1);
assert!(jet::eq_32(1, to_miles(eight_furlongs)));
assert!(jet::eq_32(1, to_miles(one_mile)));
}
```
### Built-in Functions
# Built-in functions
[SimplicityHL](../glossary.md#simplicityhl) offers several built-in functions and macros. These are additional to the hundreds of [jets](../../documentation/jets/) provided by the Simplicity Elements integration.
|