Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Sapio Language

Designing Bitcoin Contracts with Sapio

Build Status crates docs forkme

A practical guide to engineering bitcoin smart contracts using the Sapio Language.

This book is a work in progress! Please submit a PR with improvements or suggestions.

Introduction

Welcome to Designing Bitcoin Contracts with Sapio, the official manual and best starting place to learn how to make Smart Contracts for Bitcoin. Sapio is an in-development tool that empowers Bitcoin Developers to craft smart contracts in an intuitive, safe, and composable way. Sapio challenges the notion that you can’t make complex smart contracts for Bitcoin, and opens the floodgates for a myriad of new ideas to be defined easily.

Who is Sapio For?

Sapio is for anyone who wants to build with Bitcoin. That spans students demonstrating research concepts, corporations working on custody solutions, and developers improving open source solutions. Sapio is not a Solidity equivalent. The programming model is very different. But it does help anyone trying to solve a transactional protocol for Bitcoin solve it elegantly.

Sapio is currently alpha quality software. You should think very carefully before using Sapio with any real money. There will be kinks to untwist, wrinkles to iron out, and bugs to squash. Hopefully you, dear reader, will even be able to help with that! Sapio is not – at present – for the faint of heart.

What will I learn if I read this book?

This book is intended to teach you how to think about programming Sapio contracts. The book contains some exercises (that are heavily encouraged) that should instigate your understanding of how to build smart contracts for Bitcoin.

If you go through the chapters in order and complete all the exercises you should develop a firm grasp of how to use Sapio, how it works, and how it will progress over time. You will also have sufficient understanding to contribute back meaningfully to the open source project.

Getting Started

Let’s start buil… not so fast there.

Before we get into it, we need to cover some basics:

  • Setting up an environment
  • Learning Rust
  • Hello World contract

Installing Sapio

Sapio Pod QuickStart:

DOWNLOAD THE POD

Today, Sapio can come to you in an easy to set up Docker compatible container (unofficial™). With the Sapio pod you get:

  1. A CTV Compatible Bitcoin Node running regtest
  2. Rust
  3. A pre-built cached Sapio Directory for you to use as a workspace
  4. sapio-cli pre-built
  5. Sapio Studio built and running over X11 connected to your regtest node
  6. neovim for editing

See the repo for setup instructions, especially with x11 through containerization.

This is the simplest way to get a working Sapio playground, but you may prefer to have it set up locally (x11 can be glitchy). The Sapio Pod is currently targetted at someone wanting a pain free development environment for tutorials, but future releases may target more specific needs such as deployments in infrastructure.

The book will assume this is your setup, and instructions will be tailored appropriately.

Local QuickStart:

Sapio should work on all platforms, but is recommended for use with Linux (Ubuntu preferred). Follow this quickstart guide to get going.

  1. Get rust if you don’t have it already.
  2. Add the wasm target and nightly toolchain by running the below command in your terminal:
rustup target add wasm32-unknown-unknown

Tip: On macOS you may need to do the following:

brew install llvm
cargo install wasm-pack
rustup toolchain install nightly
rustup default nightly

and then load the following before compiling to use the newer llvm/clang.

export PATH="/opt/homebrew/opt/llvm/bin:$PATH"
# for older homebrew installs
# export PATH="/usr/local/opt/llvm/bin:$PATH"
export CC=/opt/homebrew/opt/llvm/bin/clang
export AR=/opt/homebrew/opt/llvm/bin/llvm-ar
  1. Clone this repo:
git clone --depth 1 git@github.com:sapio-lang/sapio.git && cd sapio

We recommend a shallow clone unless you want the full history.

  1. Build a plugin
cd plugin-example/ && cargo build --release --target wasm32-unknown-unknown && cd ..

If the compilation fails, you may want to check the clang version (8<=), and install libraries for cross-compilation (in case of ubuntu, sudo apt install gcc-multilib)

  1. Instantiate a contract from the plugin:
cargo run --bin sapio-cli -- contract create "{\"arguments\":{\"ForAddress\":{\"amount_step\":{\"Sats\":100},\"cold_storage\":\"bcrt1qumrrqgt7e3a7damzm8x97m6sjs20u8hjw2hcjj\",\"hot_storage\":\"bcrt1qumrrqgt7e3a7damzm8x97m6sjs20u8hjw2hcjj\",\"mature\":{\"RH\":10},\"n_steps\":10,\"timeout\":{\"RH\":5}}},\"context\":{\"amount\":1000,\"network\":\"Regtest\"}}" --file="plugin-example/target/wasm32-unknown-unknown/debug/sapio_wasm_vault.wasm"

You can use cargo run --release --bin sapio-cli -- help to learn more about what a the CLI can do! and cargo run --bin sapio-cli -- <subcommand> help to learn about subcommands like contract. If you aren’t modifying Sapio itself, you’ll want to run cargo build --release and use a release binary as it is much faster.

  1. Install Sapio Studio

Sapio Studio is an in-development graphical user interface for Sapio. It is the recommended way to get started with Sapio development. We recommend a shallow clone unless you want the full history.

git clone --depth 1 git@github.com:sapio-lang/sapio-studio.git && cd sapio-studio
yarn install

and then in separate shells

yarn start-react
yarn start-electron

The first time you run it you most likely will have some errors, you will need to ensure you’ve configured your client correctly. You can do this by opening the Preferences menu and configuring it appropriately. Soon there will be a better interface for first run setup.

Docs

You can review the docs either by building them locally or viewing online.

Learning Rust

A full rust tutorial is out of scope for this guide.

You may wish to begin with The Rust Programming Language.

Deep expertise in Rust is not required to be a fluent Sapio developer, but it helps. Typical Sapio programs are relatively simple as we are not typically concerned with concurrency or memory efficiency.

Hello World

Let’s get going with your very first hello world contract!

Unfortunately, until Sapio becomes a little more popular the embedded rust playground won’t work, so you’ll want to copy it locally.

We’re going to start with a contract that allows two parties, Alice and Bob, to either agree on an outcome or to default to a pre-fixed outcome after a relative timeout.

#![allow(unused)]
fn main() {
//! Hello World Contract

#![deny(missing_docs)]
#[cfg(target_arch = "wasm32")]
use sapio_wasm_plugin::{optional_logo, REGISTER};

use sapio::contract::*;
use sapio::*;
use sapio_base::amount::CoinAmount;
use sapio_base::timelocks::RelTime;
use sapio_base::Clause;
use schemars::JsonSchema;
use serde::Deserialize;
use std::convert::{TryFrom, TryInto};

/// Trustless Escrow Contract
#[derive(JsonSchema, Deserialize)]
pub struct TrustlessEscrow {
    alice: bitcoin::XOnlyPublicKey,
    bob: bitcoin::XOnlyPublicKey,
    alice_escrow_address: bitcoin::Address<bitcoin::address::NetworkUnchecked>,
    alice_escrow_amount: CoinAmount,
    bob_escrow_address: bitcoin::Address<bitcoin::address::NetworkUnchecked>,
    bob_escrow_amount: CoinAmount,
}

impl TrustlessEscrow {
    #[guard]
    fn cooperate(self, _ctx: Context) {
        Clause::And(vec![
            Clause::Key(self.alice).into(),
            Clause::Key(self.bob).into(),
        ])
    }
    #[then]
    fn use_escrow(self, ctx: Context) {
        let network = ctx.network;
        ctx.template()
            .add_output(
                self.alice_escrow_amount.try_into()?,
                &Compiled::from_address(
                    self.alice_escrow_address.clone().require_network(network)?,
                    bitcoin::Amount::ZERO,
                ),
                None,
            )?
            .add_output(
                self.bob_escrow_amount.try_into()?,
                &Compiled::from_address(
                    self.bob_escrow_address.clone().require_network(network)?,
                    bitcoin::Amount::ZERO,
                ),
                None,
            )?
            .set_sequence(
                0,
                RelTime::try_from(std::time::Duration::from_secs(10 * 24 * 60 * 60))?.into(),
            )?
            .into()
    }
}

impl Contract for TrustlessEscrow {
    declare! {finish, Self::cooperate}
    declare! {actions, Self::use_escrow}
}

#[cfg(target_arch = "wasm32")]
REGISTER![TrustlessEscrow, "logo.png"];
}

The implementation is in plugin-example/helloworld/src/plugin.rs. Its JSON addresses deserialize as Address<NetworkUnchecked> and are checked against ctx.network before becoming outputs. The fixed payout branch uses Sapio’s selected covenant backend; signer emulation includes trust in its signers.

From the repository root, build this plugin with:

cargo build --manifest-path plugin-example/Cargo.toml \
  --package sapio-wasm-helloworld --release \
  --target wasm32-unknown-unknown --locked

Use the repository’s pinned Rust toolchain and an LLVM Clang that supports wasm32 for secp256k1’s C code. On Linux, set export CC_wasm32_unknown_unknown=clang; on macOS with Homebrew LLVM installed, set export CC_wasm32_unknown_unknown="$(brew --prefix llvm)/bin/clang". The default output is plugin-example/target/wasm32-unknown-unknown/release/sapio_wasm_helloworld.wasm (unless you override Cargo’s target directory).

Challenges

For the challenges, you’ll want to modify the helloworld plugin file directly. Through this tutorial we’ll use this as a sandbox file.

  1. Add a new finish state that allows Alice to spend after a relative timeout.
  2. Add use_escrow2 which enables a different pair of payouts to Alice and Bob as an alternative.

BIP-119 CTV Fundamentals

Background

BIP-119 OP_CHECKTEMPLATEVERIFY (CTV) is a proposed soft-fork upgrade to Bitcoin for enabling a bevy of use cases.

At it’s core, CTV enables a script to commit to the “important bits” of how it can be spent, or the:

  1. nVersion
  2. nLockTime
  3. scriptSig hash (maybe!)
  4. input count
  5. sequences hash
  6. output count
  7. outputs hash
  8. input index

This enables a myriad of use cases, which are described in detail in the BIP and on the website utxos.org.

How do we think about Smart Contracts and CTV?

Before CTV, in most Bitcoin smart contracts, we think at the key-level. That is, what is a complex set of signers and satisfactions to unlock a specific coin. But once we unlock a coin, the smart contract usually does not encode any further restrictions on how it may be spent.

You could think of this as “a key to a car”. If it unlocks the car, you can take the car wherever you want.

With CTV, we hope to encode a bit more information about how coins should move by providing the paths that the coins must move through as well. So rather than just being the key to a car, you could think of it a bit more like the keys to train – still required to start the engine, but you have to stay on the tracks and there is a finite number of tracks to pick at any juncture.

That’s all a bit abstract. Think back to the Hello World example we saw earlier. We created a coin with the following options:

  1. Alice and Bob Agree \( \rightarrow \) coin goes anywhere
  2. Timeout \( \rightarrow \) coins go back to Alice and Bob

Now imagine we wanted to change the rules a little. What if instead of rule 2 apply after a timeout, what if we wanted the timeout to be measured from the time that Alice or Bob claimed they wanted to use the escrow.

This puts us in a little bit of a pickle. Sure we could just re-write the rules:

  1. Alice and Bob Agree \( \rightarrow \) coin goes anywhere
  2. Timeout since Alice or Bob requested \( \rightarrow \) coins go back to Alice and Bob

But Bitcoin doesn’t have a script level notion of “since” a part of a witness was constructed. The CTV way to think of this script is to define a state machine with two states \( S \in \{Normal, Closing\}\) and the rules:

  • \( S \gets Normal\):

    1. Alice and Bob Agree \( \rightarrow \) coin goes anywhere
    2. Alice or Bob Requested \( \rightarrow \) (\(S \gets Closing \))
  • \( S \gets Closing\):

    1. Alice and Bob Agree \( \rightarrow \) coin goes anywhere
    2. Timeout since (\(S \gets Closing\)) \( \rightarrow \) coins go back to Alice and Bob.

What drives the transition from Normal to Closing? Just a standard Bitcoin transaction!

So What is Sapio

Sapio is an embedded domain specific language for defining these sorts of state transition rules to build smart contracts for Bitcoin.

CTV is used as the mechanism to enforce that specific state transitions occur.

When we write a program in Sapio, we are designing an arbitrary state machine that can run any program.

When we compile a Sapio program, we run that state machine to completion and merkelize the resultant program states into a fixed graph.

As such, Sapio is a very powerful framework for designing Bitcoin smart contracts, but we’re constrained to the set of contracts where we can enumerate all possible end states.

To get around these restrictions, Sapio has some tricks up it’s sleeve that will be described in future chapters.

Sapio Basics

This section is intended to introduce the basic components of Sapio and how they are used. It’s a nice complement to the material available in the online docs, which are more targeted to everyday users.

Contract Guts

This section covers basic modules and primitives that are handy to know as you navigate Sapio contracts.

Feel free to skip this section and refer back to it as needed!

Miniscript & Policy

Miniscript & Policy are tools for creating well formed Bitcoin scripts developed by Blockstream developers Pieter Wiulle, Andrew Poelstra, and Sanket Kanjalkar.

From the miniscript website:

Miniscript is a language for writing (a subset of) Bitcoin Scripts in a structured way, enabling analysis, composition, generic signing and more.

Bitcoin Script is an unusual stack-based language with many edge cases, designed for implementing spending conditions consisting of various combinations of signatures, hash locks, and time locks. Yet despite being limited in functionality it is still highly nontrivial to:

  1. Given a combination of spending conditions, finding the most economical script to implement it.
  2. Given two scripts, construct a script that implements a composition of their spending conditions (e.g. a multisig where one of the “keys” is another multisig).
  3. Given a script, find out what spending conditions it permits.
  4. Given a script and access to a sufficient set of private keys, construct a general satisfying witness for it.
  5. Given a script, be able to predict the cost of spending an output.
  6. Given a script, know whether particular resource limitations like the ops limit might be hit when spending.

Miniscript functions as a representation for scripts that makes these sort of operations possible. It has a structure that allows composition. It is very easy to statically analyze for various properties (spending conditions, correctness, security properties, malleability, …). It can be targeted by spending policy compilers (see below). Finally, compatible scripts can easily be converted to Miniscript form - avoiding the need for additional metadata for e.g. signing devices that support it.

For Sapio, we use a customized fork of rust-miniscript library which extends miniscript with functionality relevant to CheckTemplateVerify and Sapio. All changes should be able to be upstreamed… eventually.

The Policy type (named Clause in Sapio) allows us to specify the predicates upon which various state transitions should unlock.

This makes it so that Sapio should be compatible with other software that can generate valid Policies, and compatible with PSBT signing devices that understand how to satisfy miniscripts.

A limitation of this approach is that there are certain types of script which are possible, but not yet supported in Sapio. For example, the OP_SIZE coin flip script is not currently possible with Miniscript. Another limitation of Miniscript is that keys may not be repeated to preserve a guarantee of non malleability.

Miniscript & Policy are an ongoing research concern. As they develop, Sapio will benefit from this foundational work.

Transaction plans

Start with a plan when a contract needs exact payments, change, a fee budget or named sponsor inputs. The plan resolves allocations before compiling children, then freezes the exact ordered transaction into the compiler’s usual template.

The runnable payment example uses ordinary Rust methods under #[sapio::contract]. Its committed action declares one recipient and reserves a fee. Try it from the repository root:

cargo run --locked -p sapio --example payment

The complete source below is included directly from the tested example:

//! Compile a small payment contract without a node, signer, or WASM runtime.
//! Native CTV is a research target; this example does not fund or broadcast it.
use bitcoin::{Address, Amount, Network};
use sapio::contract::{Compilable, CompilationError, Compiled, Context};
use sapio::template::{OutputAmount, Template};
use sapio_base::covenant::LoweringPlan;
use sapio_base::effects::EffectPath;
use std::sync::Arc;

struct Payment {
    destination: Compiled,
}

#[sapio::contract]
impl Payment {
    #[action(committed)]
    fn pay(&self, ctx: Context) -> Result<Template, CompilationError> {
        let mut plan = ctx.template_plan();
        plan.output(
            "recipient",
            OutputAmount::Exact(Amount::from_sat(1_000)),
            &self.destination,
        )?;
        plan.reserve_fees(Amount::from_sat(500));
        plan.require_feerate(bitcoin::FeeRate::from_sat_per_vb(1).unwrap());
        Ok(plan.finish()?)
    }
}

fn compile_payment(funding: u64) -> Result<Compiled, Box<dyn std::error::Error>> {
    let destination = "bcrt1qumrrqgt7e3a7damzm8x97m6sjs20u8hjw2hcjj"
        .parse::<Address<bitcoin::address::NetworkUnchecked>>()?
        .require_network(Network::Regtest)?;
    let contract = Payment {
        destination: Compiled::from_address(destination, bitcoin::Amount::ZERO),
    };
    let compiled = contract.compile(Context::new(
        Network::Regtest,
        Amount::from_sat(funding),
        LoweringPlan::Native,
        EffectPath::try_from("payment")?,
        Arc::new(Default::default()),
        None,
    ))?;
    compiled.validate()?;
    Ok(compiled)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let compiled = compile_payment(1_500)?;
    serde_json::to_writer_pretty(
        std::io::stdout().lock(),
        &serde_json::json!({
            "network": "regtest",
            "enforcement": "native_ctv_research",
            "funding_satoshis": 1_500,
            "contract": compiled,
        }),
    )?;
    Ok(())
}

#[test]
fn payment_preserves_destination_value_and_fee_reserve() {
    let compiled = compile_payment(1_500).unwrap();
    assert_eq!(compiled.ctv_to_tx.len(), 1);
    let template = compiled.ctv_to_tx.values().next().unwrap();
    assert_eq!(template.tx.input.len(), 1);
    assert_eq!(template.tx.output.len(), 1);
    assert_eq!(template.tx.output[0].value.to_sat(), 1_000);
    let destination = "bcrt1qumrrqgt7e3a7damzm8x97m6sjs20u8hjw2hcjj"
        .parse::<Address<bitcoin::address::NetworkUnchecked>>()
        .unwrap()
        .require_network(Network::Regtest)
        .unwrap();
    assert_eq!(
        template.tx.output[0].script_pubkey,
        destination.script_pubkey()
    );
    assert_eq!(template.required_input_amount.to_sat(), 1_500);
    assert!(compile_payment(1_499).is_err());
}

OutputAmount::Remainder can assign change to one additional output. Output order is declaration order; the resolver never sorts destinations. Auxiliary inputs have names and declared minimum contributions, checked again when funding is supplied. Repeated fee reservations take their maximum, and height/time lock conflicts are explicit errors.

A fee cap is a local preparation rule. It is not automatically enforced by Bitcoin Script. Final fee-rate checks also need actual previous outputs and the completed witness weight. Native signature finalization and chain checks remain separate from transaction construction.

The lower-level builder described next is useful for procedural constructions. Both APIs produce the same Template.

Template Builder

The template builder defines a transaction step. Each output receives part of the available funding and its own compilation context. Reserve fees explicitly after adding all outputs.

This contract pays 1,000 sats, returns the remaining funds as change, and reserves 100 sats for fees:

#![allow(unused)]
fn main() {
use bitcoin::{Amount, XOnlyPublicKey};
use sapio::contract::{CompilationError, Contract};
use sapio::{declare, then, Context};

struct Payment {
    recipient: XOnlyPublicKey,
    change: XOnlyPublicKey,
}

impl Payment {
    #[then]
    fn pay(self, ctx: Context) {
        let fee = Amount::from_sat(100);
        let mut tmpl = ctx
            .template()
            .set_label("Payment".into())
            .add_output(Amount::from_sat(1_000), &self.recipient, None)?;

        let change = tmpl
            .ctx()
            .funds()
            .checked_sub(fee)
            .ok_or(CompilationError::OutOfFunds)?;
        if change != Amount::ZERO {
            tmpl = tmpl.add_output(change, &self.change, None)?;
        }

        tmpl.add_fees(fee)?.into()
    }
}

impl Contract for Payment {
    declare! {actions, Self::pay}
}
}

Builder methods consume the previous builder and return the updated one. ctx().funds() reports the remaining construction budget. Emitted outputs and explicitly reserved fees determine the template’s minimum funding requirement; an unused budget is not a fee reservation.

add_output passes the output’s amount to the receiving contract. When ordinal ranges are known, outputs receive consecutive prefixes in transaction input order. Debiting the builder without creating an output would shift those ordinal assignments, so direct spend_amount access is private.

add_fees records the fee and changes the builder’s state. That state allows additional fee reservations within the remaining budget, but cannot add more outputs or auxiliary funds. This keeps fees after all outputs. Complete metadata, guards, input sequences and change outputs before reserving fees.

Auxiliary input contributions use add_sequence().add_amount(amount)?; these funds are separate from the contract input’s required contribution. When input ordinals are tracked, allocate all known sats to outputs before introducing unknown auxiliary funds. Binding checks the actual funding inputs.

Sapio currently places the contract’s UTXO at input zero. The CTV commitment includes this index. See the builder implementation for the complete set of operations.

Time Locks

Sapio provides typed absolute and relative timelocks in sapio_base::timelocks. Heights and times have separate constructors so a value cannot silently change its interpretation.

use sapio_base::timelocks::*;
use sapio_base::Clause;
use std::convert::{TryFrom, TryInto};
use std::time::Duration;

fn main() -> Result<(), LockTimeError> {
let height = AbsHeight::try_from(800_000u32)?;
let timestamp = AbsTime::try_from(1_000_000_000u32)?;
let same_timestamp = AbsTime::try_from(Duration::from_secs(1_000_000_000))?;

// Relative time is encoded in intervals of 512 seconds.
let intervals = RelTime::from(10u16);
let duration = RelTime::try_from(Duration::from_secs(10 * 512))?;
let blocks = RelHeight::from(20u16);

// Converting a transaction timelock into a policy is fallible.
let older: Clause = blocks.try_into()?;
let after: Clause = height.try_into()?;

let relative: AnyRelTimeLock = blocks.into();
let any: AnyTimeLock = relative.into();
let also_older = Clause::try_from(any)?;
Ok(())
}

RelTime::try_from(Duration) rounds up to whole 512-second intervals, so a fractional interval cannot make the lock mature early. AbsTime rounds a fractional timestamp up to a whole second. Both reject durations outside their encoded range. The types’ JSON values are their encoded consensus fields; relative time includes the time-type flag, not just a count of seconds.

Transaction fields and policy guards

Transaction fields and Miniscript predicates have different valid domains. Sapio’s absolute transaction locks cover heights 0..500_000_000 and timestamps 500_000_000..=u32::MAX. Relative heights and time-interval counts fit in a u16. These types can be passed to the template builder without converting them into policy clauses.

Miniscript’s typed absolute guards accept encoded values 1..=0x7fff_ffff. Its relative guards require an enabled BIP68 encoding and a nonzero encoded operand. For example, a zero-block sequence is a valid transaction field, but cannot become an Older policy; a timestamp above 0x7fff_ffff also cannot become an After policy:

#![allow(unused)]
fn main() {
use sapio_base::timelocks::{AbsTime, RelHeight};
use sapio_base::Clause;

assert!(Clause::try_from(RelHeight::from(0u16)).is_err());
let timestamp = AbsTime::try_from(u32::MAX).unwrap();
assert!(Clause::try_from(timestamp).is_err());
}

Use Clause::try_from(lock) or lock.try_into() and propagate LockTimeError::InvalidPolicyLockTime when building a guard. The Any*TimeLock wrappers use the same fallible conversion. If constructing Miniscript clauses directly, use its typed lock constructors; Sapio also rejects the exposed RelLockTime::ZERO constant during policy validation.

Sats and Coins

Sapio uses integer satoshis for transaction values. At a JSON boundary, the interface must also make the denomination clear: 10 could otherwise mean 10 satoshis or 10 bitcoin.

Units and serialization

These types serve different purposes:

TypeRepresentation and JSON behavior
u64An unsigned integer; the interface must specify its unit.
i64A signed integer; the interface must specify its unit.
bitcoin::AmountUnsigned integer satoshis. With Bitcoin’s serde feature, its default JSON representation is an integer number of satoshis.
bitcoin::SignedAmountSigned integer satoshis. Use an explicit bitcoin::amount::serde adapter for JSON fields.
sapio_base::amount::CoinAmountA tagged input: {"Sats": 1000} or {"Btc": 0.00001}.

CoinAmount belongs to Sapio, not Bitcoin. Convert it to Amount before using it in a transaction. The Btc variant uses Amount::from_btc to check the conversion, including range and fractional-satoshi precision; Sats preserves the supplied integer exactly.

#![allow(unused)]
fn main() {
use bitcoin::Amount;
use sapio_base::amount::CoinAmount;

let amount = Amount::try_from(CoinAmount::Sats(1000)).unwrap();
assert_eq!(amount.to_sat(), 1000);
assert_eq!(serde_json::to_string(&amount).unwrap(), "1000");
}

A type’s integer range is not a contract budget or Bitcoin’s monetary limit. Validate amounts against the funds available and the rules of the interface.

Binary floating point cannot represent most decimal bitcoin fractions exactly. Prefer integer satoshis for arithmetic and JSON interfaces you control. In JavaScript, integers up to 2^53 - 1 are exact, which covers Bitcoin’s maximum supply expressed in satoshis, but does not cover every u64 value. JSON itself does not require consumers to use floating point.

Choosing a different wire format

Amount already works in a Vec<Amount> without a custom serializer. An explicit wrapper is useful when an external interface requires another format, such as a floating-point number of bitcoin. Its schema must describe that chosen format too:

#![allow(unused)]
fn main() {
use bitcoin::Amount;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// Serialize this interface's amounts as a number of bitcoin.
#[derive(
    Serialize, Deserialize, JsonSchema, Clone, Copy, Debug, Ord, PartialOrd, PartialEq, Eq,
)]
#[serde(transparent)]
struct AmountF64(
    #[schemars(with = "f64")]
    #[serde(with = "bitcoin::amount::serde::as_btc")]
    Amount,
);

impl From<Amount> for AmountF64 {
    fn from(amount: Amount) -> Self {
        Self(amount)
    }
}

impl From<AmountF64> for Amount {
    fn from(amount: AmountF64) -> Self {
        amount.0
    }
}
}

Ordinary Amount fields derive an integer-satoshi schema directly through Sapio’s sapio-jsonschema fork and its bitcoin032 feature. Explicit Serde adapters still need matching schema annotations: as_sat uses u64, while as_btc uses f64. Keep the serializer, schema and interface documentation consistent when choosing a denomination.

Checked arithmetic

Amount arithmetic operators can panic on overflow or underflow. Use checked_add, checked_sub and the other checked operations when values come from callers, and propagate an error when a calculation cannot be represented. Keep calculations in integer satoshis even when the wire format uses bitcoin.

Contract Actions

Contracts have a variety of different actions used at different times.

namefunction
guardCreate a clause using miniscript with access to the contract’s values and context.
compile_ifDetermine if a then or finish should be compiled based on the contract’s values and context
thenCreate a path or paths that are guaranteed using CTV for a contract to be spent, with optional guards and compile_ifs.
continuationCreate a suggested path or paths for a contract to be spent that are not guaranteed via CTV with mandatory guards and compile_ifs. Also accepts an update argument for generating transactions based on future data.
decl_*!For any of the above, declare the existence of a method, for e.g. a trait definition, without defining the function.

This section will teach you then ins and outs of each.

Guard

A guard is a fixed spending predicate. It can use native Miniscript, a typed program evaluator, or another supported policy compiler.

Inside #[sapio::contract], a #[policy] method declares a reusable predicate. A #[spend] method additionally exports its predicate as independently sufficient to spend the output. An action attaches policies with guarded_by(Self::signed, Self::timeout); these predicates are conjoined.

#![allow(unused)]
fn main() {
#[policy]
fn signed(&self) -> Clause {
    Clause::Key(self.owner)
}
}

The ordinary method’s signature expresses its context dependency. With only &self, it is cached within the compilation. Adding Context evaluates it at each attachment. The method remains directly callable as Rust.

The standalone #[guard] frontend is useful for trait interfaces and optional metadata callbacks:

An ordinary guard receives the context of each attachment. Use it when the policy depends on the current path or other compilation context.

A cached guard is evaluated once per guard declaration during a contract’s compilation. It receives only self, so it cannot accidentally reuse the first attachment’s context at a different path. Each compilation starts a new cache; compiling the same contract again evaluates the cached policy again.

guard macro

#![allow(unused)]
fn main() {
#[guard]
fn contextual(self, ctx: Context) {
    // Compute a Clause using this attachment's context.
}

#[guard(cached)]
fn signed(self) {
    Clause::Key(self.owner)
}
}

These methods belong inside the contract’s implementation. Trait interfaces can declare their corresponding optional methods with decl_guard! { contextual } and decl_guard! { cached signed }.

Guard metadata remains contextual even when the policy is cached. A simps = "Some(Self::metadata)" callback receives its own Context for every attachment, including standalone finish guards. Its errors abort compilation. The compiled artifact groups annotations by guard clause and protocol number, preserving distinct JSON values in attachment order and removing equal values. Metadata from different attachment paths therefore remains visible without duplicating identical annotations.

Compile-time action conditions

ConditionallyCompileIf enables a contract writer to evaluate certain value-based logic before evaluating a path function.

If the return value(s) indicate that a branch should not be evaluated, it is skipped.

When to Use ConditionallyCompileIf

Suppose we’re creating a super secure wallet vault, and we want a recovery path that’s only accessible if the amount of funds being sent to the contract is < an amount.

We could write:

#![allow(unused)]
fn main() {
#[condition]
fn not_too_much(&self, ctx: Context) -> ConditionalCompileType {
    if ctx.funds() > Self::MAX_FUNDS {
        ConditionalCompileType::Never
    } else {
        ConditionalCompileType::NoConstraint
    }
}
}

Inside #[sapio::contract], apply it with #[action(committed, compile_if(Self::not_too_much))]. This controls whether the compiler includes the action. It does not introduce a predicate checked while spending; spending rules belong in policies.

ConditionalCompileType Variants

There are many different ConditionalCompileType return values:

#![allow(unused)]
fn main() {
pub enum ConditionalCompileType {
    /// May proceed without calling this function at all
    Skippable,
    /// If no errors are returned, and no txtmpls are returned,
    /// it is not an error and the branch is pruned.
    Nullable,
    /// The default condition if no ConditionallyCompileIf function is set, the
    /// branch is present and it is required.
    Required,
    /// This branch must never be used
    Never,
    /// No Constraint, nothing is changed by this rule
    NoConstraint,
    /// The branch should always trigger an error, with some reasons
    Fail(LinkedList<String>),
}
}

These values are merged according to specific “common sense” logic. Please see ConditionalCompileType::merge for details.

#![allow(unused)]
fn main() {

    ///     Fail > non-Fail ==> Fail
    ///     forall X. X > NoConstraint ==> X
    ///     Required > {Skippable, Nullable} ==> Required
    ///     Skippable > Nullable ==> Skippable
    ///     Never >< Required ==> Fail
    ///     Never > {Skippable, Nullable}  ==> Never
}

Optional interface conditions

The standalone attribute and declaration macro support optional trait methods:

#![allow(unused)]
fn main() {
#[compile_if]
fn available(self, ctx: Context) -> ConditionalCompileType {
    ConditionalCompileType::NoConstraint
}

// In a trait interface, its factory is absent unless implemented:
decl_compile_if! { available }
}

Both frontends preserve the same condition algebra. Never and Required contradict one another; an explicit failure does not hide this contradiction. An absent condition factory keeps the declared slots of the remaining conditions, preserving their context paths.

Committed actions

A committed action constructs the transactions permitted by a covenant. Each returned transaction is combined with the action’s authorization guards and the configured covenant lowering. This is useful for fixed payouts, timeout paths, and recursively constructed transaction trees.

#![allow(unused)]
fn main() {
#[sapio::contract]
impl Escrow {
    #[action(committed)]
    fn refund(&self, ctx: Context) -> Result<Template, CompilationError> {
        self.refund_template(ctx)
    }
}
}

An argument-free committed action produces its default transaction during compilation. A request-taking committed action requires supplied requests or an explicit default-proposal callback. Its requests participate in compilation of the output’s fixed spending policy: changing them may change the contract’s address.

guarded_by(Self::authorization) attaches fixed policies. compile_if(Self::availability) applies a separately declared #[condition] method returning ConditionalCompileType. The normal required action must produce at least one template; Nullable permits an empty result and Never omits the action.

A method can return a Template or Result<Template, CompilationError>. Multiple alternatives are explicit through Vec<Template> or Result<Vec<Template>, CompilationError>; TxTmplIt remains available for a fallible stream.

The standalone #[then] frontend remains useful when implementing an optional trait action declared with decl_then!. It creates the same committed action representation, with an argument-free default callback. Export it through declare! {actions, Self::refund}.

Suggested actions

A suggested action constructs candidate transactions under a fixed spending policy. Its authorization guards determine who can spend; returning a candidate from Rust does not commit the output to that transaction.

For example, an escrow’s participants may agree to make a payment and return the remainder to a new escrow. The action provides shared construction logic, while the participants’ signatures authorize the actual transaction.

#![allow(unused)]
fn main() {
#[sapio::contract]
impl Escrow {
    #[policy]
    fn participants(&self) -> Clause {
        Clause::And(vec![Clause::Key(self.alice).into(), Clause::Key(self.bob).into()])
    }

    #[action(suggested, guarded_by(Self::participants))]
    fn pay(&self, ctx: Context, payment: Payment) -> Result<Template, CompilationError> {
        self.payment_template(ctx, payment)
    }
}
}

Payment is this action’s own request type. The generated Escrow::pay_action() handle can invoke the method with a typed request or encode it for compilation through JSON/WASM. There is no contract-wide argument enum.

No request means no invocation. A request-taking action does not need a Default implementation or an artificial Option parameter. Even a unit request is an explicit request: JSON null is distinct from an absent effects entry.

When useful, defaults = Self::default_proposals attaches a separate callback that constructs default candidates. It does not manufacture request arguments. An argument-free suggested action can use the explicit default flag instead.

The method’s Rust checks validate the generated candidate. Spending requirements belong in its policy or evaluator; the compiler rejects attempts to add new authorization guards through a suggested template.

The standalone #[continuation] frontend remains available for trait interfaces. It generates the same Action<Self, Request> representation, accepts optional defaults = "Self::default_proposals", and exposes JSON when marked web_api.

When to use macros

Use #[sapio::contract] on an inherent impl for normal contract authoring. Mark transaction methods with #[action(committed)] or #[action(suggested)], policies with #[policy], and independently sufficient spending policies with #[spend]. The methods remain ordinary Rust methods. The macro generates typed action handles, request schemas and registration.

A policy with only &self is context-free and cached. A policy that also accepts Context is evaluated at its attachment context. The signature expresses the actual dependency.

The explicit low-level API is Action<Contract, Request>. Its constructor takes a transaction-generation callback. with_guards, with_conditions, with_json, and with_defaults add the corresponding capabilities; erase() hides only the individual request type when registering the action. There is no global argument pack. This API is useful for dynamically assembled contracts.

Optional interfaces can still use decl_then!, decl_continuation!, and decl_guard!, implemented by the standalone action/guard attributes. A factory returning None means that the implementation does not provide that interface member. Export optional factories explicitly through declare! {actions, ...} or #[sapio::contract(actions(Self::optional), spends(Self::optional_guard))].

Use #[condition] with compile_if(...) for availability depending on the contract’s value. Its condition algebra distinguishes absent, nullable and required branches; it does not add a spending predicate.

Contract declarations

#[sapio::contract] registers explicitly marked actions and spending policies. Ordinary helper methods remain ordinary helpers. #[policy] declares a policy that can guard actions; #[spend] also exports it as independently sufficient to spend the output.

Contract hooks are ordinary methods marked #[amount] for minimum funding, #[internal_key] for an already authorized Taproot internal key, and #[metadata] for descriptive object metadata. Selecting an internal key never grants new spending authority.

An explicit Contract implementation can register factories directly:

#![allow(unused)]
fn main() {
impl Contract for Escrow {
    declare! {actions, Self::refund, Self::propose_payment}
    declare! {finish, Self::cooperative}
}
}

Each action has its own request type before registration. A factory returns Option<Box<dyn ErasedAction<Self>>>; returning None explicitly omits an optional interface member. Action names must be unique within a contract so typed request paths cannot be silently redirected.

DynamicContract<S> assembles actions and independent finish factories in vectors alongside the contract data, metadata callback and minimum-funding callback. A custom AnyContract implementation can provide the same compiler interface without choosing a specific storage layout.

Existing addresses can be used through Compiled::from_address. They provide an output destination and minimum-funding information, but no action API or source policy beyond the supplied artifact.

Contract Compilation Overview

When the compiler sees a new contract, it proceeds by processing each path item one at a time. If the order of compilation is important for your contract:

  1. reconsider your priorities
  2. repeat step 1
  3. read the logic inside of the Compilable::compile function

This logic may be improved over time to take advantage of parallelization or otherwise restructure. As such, one should be careful when switching compiler versions. Further, optimizers or data structures may be unstable with respect to things like renamed functions leading to changes of compilation result.

Determinism?

Sapio is designed to be determinism-friendly. Repeated runs of the same program should – unless the user includes entropy – return the same results.

However, at writing, this property is not closely audited for, so outputs should be treated as required to be stored in order to use a contract.

On the other hand, determinism means that for multi-party contracts being generated in a Replicated state machine, if all parties have the same e.g. WASM plugin, they can generate a contract definition and check that the merkle root (in this case, a bitcoin address) is the same. If it differs, either the arguments differed, someone cheated, or there was unexpected non-determinism.

Typed action requests and effects

A contract’s spending policy can be compiled without making a request to every action. Each suggested action publishes its own argument schema and effects path. The generated typed handle encodes a request at that exact path:

#![allow(unused)]
fn main() {
let effects = Escrow::pay_action().request(&root_path, &payment)?;
let context = Context::new(
    network,
    amount,
    lowering,
    root_path,
    Arc::new(effects),
    None,
);
let artifact = escrow.compile(context)?;
}

The request invokes only pay; it is not passed through unrelated actions or a shared contract-wide enum. JSON deserialization occurs at the request boundary. Escrow::pay_action().invoke(&escrow, context, payment) is the equivalent typed Rust construction entry point. Directly calling escrow.pay(context, payment) also works because the contract macro preserves ordinary methods.

For several candidates at the same action, use Escrow::pay_action().requests(&root_path, &payments). Entries receive distinct, deterministic labels that preserve their input order. An empty collection adds no requests. The lower-level MapEffectDB remains available when requests must be attached at multiple contract paths.

An argument-free suggested action still needs an explicit unit request, encoded as JSON null. An absent entry never means “call this action with a default value.” Explicit default-proposal callbacks are evaluated separately.

Suggested transactions remain subject to their fixed authorization policy. A candidate does not acquire authority because its generator accepted it. For example, an NFT sale generator may construct the transfer and seller payment; the owner must still authorize the resulting transaction through the spending policy. A committed action additionally fixes its candidate transactions in the compiled covenant, so changing committed candidates may change the address.

Raw effects paths that are not visited by compilation are not automatically reported as unused. Typed handles avoid hand-written action paths; callers that assemble raw effects maps remain responsible for targeting the intended contract instance.

Sapio for Fun (and Profit)

In this section, we’re going to build a simple option contract. This sort of contract could be used, for example, to make an on-chain asynchronous offer to someone to enter a bet with you.

Then, you’ll have some challenges to modify the contract to extend it’s functionality meaningfully.

The logic for the basic contract is as follows:

  1. If \(\tau_{now} > \tau_{timeout} \):
    • send funds to return address
  2. If strike_price btc are added:
    • send funds + strike_price to strike_into contract
#![allow(unused)]
fn main() {
/// The Data Fields required to create a on-chain bet
pub struct UnderFundedExpiringOption {
    /// How much money has to be paid to strike the contract
    strike_price: Amount,
    /// if the contract expires, where to return the money
    return_address: bitcoin::Address,
    /// if the contract strikes, where to send the money
    strike_into: Box<dyn Compilable>,
    /// the timeout (as an absolute time) when the contract should end.
    timeout: AnyAbsTimeLock,
}

impl UnderFundedExpiringOption {
    #[then]
    /// return the funds on expiry
    fn expires(self, ctx: Context) {
        ctx.template()
            // set the timeout for this path -- because it is using
            // a committed action needs no separate timelock guard.
            .set_lock_time(self.timeout)?
            .add_output(
                // ctx.funds() knows how much money has been sent to this contract
                ctx.funds(),
                // this bootstraps an address into a contract object
                &Compiled::from_address(self.return_address.clone(), None),
                None,
            )?
            .into()
    }
    /// continue the contract
    #[then]
    fn strikes(self, ctx: Context) {
        let tmpl = ctx.template().add_amount(self.strike_price);
        let amt = (tmpl.ctx().funds() + self.strike_price).into();
        tmpl.add_sequence()
            .add_output(
                // use the inner context of tmpl because it has added funds
                amt,
                self.strike_into.as_ref(),
                None,
            )?
            .into()
    }
}

impl Contract for UnderFundedExpiringOption {
    declare!(actions, Self::expires, Self::strikes);
}
}

Challenges

There’s no right answer to the following challenges, and the resulting contract may not be too useful, but it should be a good exercise to learn more about writing Sapio contracts.

  1. Clear out your helloworld plugin and put this code in.
  2. Write a contract designed to be put into the strike_into field which sends funds to one party or the other based on a third-party revealing a hash preimage A or B.
  3. Modify the contract so that there is a expire_A and a expire_B path that go to different addresses, and expire_A requires a signature or hash reveal to be taken.
  4. Modify the contract so that if expire_A is taken, a small payout early_exit_fee: bitcoin::Address is made to a early_exit : bitcoin::Address.
  5. Modify the contract so that expire_A is only present the fields required by it are Option::is_some (hint: use compile_if!).
  6. Add logic to deduct fees.
  7. Add a cooperative_close guard! clause that allows both parties to exit gracefully

Limitations of Sapio

Sapio is rapidly maturing, but it is still early-days for bitcoin smart contract compilers, and early-days for Sapio in particular. As such, one should be careful to closely audit smart contracts designed with Sapio, be careful to only use such contracts with trusted inputs, and in general take precautions to ensure security of funds. As Sapio matures and we gain confidence in smart contracts built with it, Sapio should be able to greatly improve the security for many bitcoin users and applications.

Other than these general “alpha software” disclaimers, Sapio is designed with certain upgrades to Bitcoin in mind that have not yet landed. While Sapio is designed to work even without these upgrades, the functionality is severely reduced or has a different security model.

This section goes over some of these limitations and when we might expect to see them addressed.

BIP-119 Emulation

Changes to Bitcoin take a long time. The star player in making Sapio work is BIP-119, and that might take a while to get merged. To get around this, Sapio provides some tools to enable similar functionality today by emulating BIP-119 with signatures.

Compilation inputs and runtime signers

Compilation takes a public LoweringPlan containing either Native or CtvEmulation { signers, threshold }. The signer entries are extended public keys. Only explicit Emulatable(Ctv(hash)) predicates follow that plan; ordinary native CTV clauses and raw scripts retain their meaning. Transaction actions add an emulatable CTV predicate automatically.

Module arguments must include context.lowering. Nested modules receive the same explicit inputs, and compilation does not connect to a signer or ask a host to choose a policy. WASM compilation hosts expose no signing capability.

The CTV emulator implementations provide runtime signing services. Binding checks that the configured runtime signer reproduces the policies selected by the artifact’s recorded public inputs. Keeping the seed requires trusting its custodian; deleting it requires collecting every signature that will be needed first. Public lowering inputs alone do not establish availability or key deletion.

This crate also defines logic for servers that want to offer emulator services to binding and signing clients. This is convenient since the emulator server must be kept secure, so an organization may want it to be more tightly safeguarded.

The emulator definitions include wrapper types that compose individual instances of an emulator into a federated multisig. This is useful for circumstances where a contract is between e.g. 2 parties and both have an emulator server. Then the contract can be “immutable” unless both collude.

Configure your own runtime signers separately from the compilation inputs. The CLI requires an explicit signer or native research mode; a missing or disabled emulator configuration no longer selects native CTV implicitly.

How it works

See the source code for more detailed documentation.

CheckTemplateVerify essentially functions as a self-signed transaction. I.e., imagine you could create a public key that could only ever sign a transaction which matched a certain pattern?

To implement this functionality, we use BIP-32 HD keys with public derivation.

On initialization, a server picks a seed S and generates a root public key K from it, and publishes K.

Users generate a transaction T and extract the CheckTemplateVerify hash H for it. They then take H and convert it into a derivation path D of 8 u32’s and 1 u8 for non-hardened derivation (see hash_to_child_vec).

This derivation path is then applied to K to generate a key C. This key is added with a CheckSig(SIGHASH_ALL) to the script in place of a CTV clause.

Then, when a user desires to spend an output with such a key, they create the entire transaction they want to occur and send it to the emulator server.

Without even checking to see that the key is used in the transaction, the server generates the template hash H’ (which should equal H) and then signs, returning the signature to the client.

Before creating a contract, clients may wish to collect all possible signatures required to prevent an availability fault.

This scheme has the benefit that:

  1. contract specification can occur without any online processes
  2. The server has no intelligent logic, all guarantees are structural.
  3. Server is completely stateless.
  4. Availability/malfeasance can be controlled for with multisig
  5. 1:1 functionality mapping to CTV

The downside of this approach to emulation is that:

  1. It is somewhat inefficient for scripts which have many branched possibilities.
  2. No inherent mechanism to delete keys after use to protect against future exfiltration.

Why BIP-32

We use BIP-32 because it is a well studied primitive and derivation paths are compatible with existing signing hardware. While it is true that a tweak of 32 bytes could be directly applied to the key more efficiently, easier interoperability with existing tools seemed to be the best path.

Customizing Emulator Trait

The emulator trait belongs to binding and signing. get_signer_for advertises the runtime’s policy for compatibility checks, and sign supplies signatures. Compilation derives policies locally from LoweringPlan and never invokes this trait. A custom implementation does not add support for arbitrary encumbrance programs; those require their own defined evaluator and protocol.

As a user of the Sapio library, you can define your own custom emulator logic but that’s out of scope of this book.

Future Work

There is a plan to make emulation more efficient based on Merkelization, but it is not yet implemented because it messes with the current way the compiler works.

The efficiency issues are also solvable, more or less, with taproot.

Taproot

Sapio contract logic can become very large in size, so Sapio benefits from being able to split up and merkelize the logic into smaller satisfiable chunks. This makes it much more economical and easy to use Sapio however you like.

Generally speaking, a Sapio programmer need not think about this too much, it will be set up automatically under the hood. However, at writing, limited optimizing of Taproot trees is done, so a wise programmer would want to express their program in such a way to not allow Taproot leafs to be larger than need be.

Advanced Transaction Handling

Sapio does not try to handle all possible types of Bitcoin transactions.

There are certain “advanced techniques” that have use cases, but are difficult to reason about. For example, there are many ways that SIGHASH flags can be exploited to create all sorts of possibilities. You can use OP_2DUP OP_SHA256 <H1> OP_EQUALVERIFY OP_SWAP OP_SHA256 <H2> OP_EQUALVERIFY OP_SIZE OP_SWAP OP_SIZE OP_EQUAL (or something similar) to flip a fair coin between participants. There is a lot.

But Sapio doesn’t make an effort to cleanly handle all possible contracts. It makes an effort to address a safe and useful subset and make those contracts well integrated with other standard software.

If you identify a killer use-case contract, please open an issue or a PR to discuss the new functionality and how to add it.

Mempool & Fees

The Mempool is a treacherous place. If you’re not familiar, the Mempool is Bitcoin’s backlog of unconfirmed transactions. It is a bounded queue which makes a best effort at storing transactions that pay higher fees and dropping transactions which pay insufficient fees.

The Mempool is an issue for a Sapio user because Sapio contracts are generally immutable, which implies that Sapio contracts have to estimate the minimum feerates at the time of contract creation.

For example, suppose I make a contract that has a state transition paying a 200 sats per vbyte feerate. And then by the time that transaction reaches the mempool, it has gone up to 201 sats per vbyte minimum. Now I cannot easily broadcast my transaction, and it is unlikely to wind up in a block.

There are many other ways that transactions can end up stuck.

Fortunately, there are some solutions to these sorts of problems, but none of them are exactly “easy”. We’ll divide them in three categories:

Careful Contract Programming

Careful contract programming can ensure that:

  1. All contract transitions pay a high enough minimum we expect to be able to get into the mempool in the future
  2. There are ways to inject “gas inputs” into the contract, if needed
  3. There are ways to spend “gas outputs” from the contract just for Child-Pays-For-Parent logic.
  4. Relative timelocks are used to prevent pinning attacks

For a discussion of this topic with visuals, please see the Sapio Reckless VR Talk section on fees:

TODO: Integrate this content into writing

P2P Network/Mempool Policy Changes

Package Relay is a proposed technique that is progressing for Bitcoin whereby multiple transactions can be submitted in one bundle to show suitability for the mempool. Therefore a contract leaf node might be able to demonstrate, by spending the coin, that the contract interior nodes are worth mining.

However, this technique is limited insofar as contract interior nodes in Sapio may commonly have relative time locks (or similar) which prevent the mempool from considering dependents.

Package Relaying does, however, improve the function of intentional gas outputs.

Consensus Changes

Consensus changes are very difficult to create, but it’s possible that in the future some set of consensus changes help decouple contract execution from fee paying.

For example, there is a proposal to replace Replace-By-Fee and Child-Pays-For-Parent with a mechanism that functions as a virtual CPFP link. However, such proposals can introduce subtle changes to Bitcoin’s behavior and must be vetted closely.

Application Packaging

So you’ve written a Sapio contract and you’re ready to get it out into the world.

How should you release it? How should you use it?

This section covers various ways to deploy and use Sapio contracts.

In general, it is important to make the code available in an open source way, so others can integrate and use your contracts. Rust’s crates system provides a natural place to publish for the time being, although in the future we may build a Sapio specific package manager as smart contracts have some unique differences.

WASM

WASM is “WebAssembly”, or a standard for producing bytecode objects that can be run on any platform. As the name suggests, it was originally designed for use in web browsers as a compiler target for any language to produce code to run safely from untrusted sources.

So what’s it doing in Sapio?

WASM is designed to be cross platform and deterministic, which makes it a great target for smart contracts that we want to be able to be reproduced locally. Sapio validates guest memory access and applies execution fuel, memory/table caps and nested-call limits before running a module. These bounds cover guest execution; native compilation and external services need their own resource policy. Loading a module is not a guarantee that its contract is safe or that its intended covenant is enforced on the selected chain.

Sapio Contract objects can be built into WASM binaries very easily. The code required is basically:

#![allow(unused)]
fn main() {
/// MyContract must support Deserialize and JsonSchema
#[derive(Deserialize, JsonSchema)]
struct MyContract;
impl Contract for MyContract{\*...*\};
/// binds to the plugin interface -- only one REGISTER macro permitted per project
REGISTER![MyContract];
}

See the example for more details.

These compiled objects require a special environment to be interacted with. That environment is provided by the Sapio CLI as a standalone binary. It is also possible to use the interface provided by the sapio-wasm-plugin crate to load a plugin from your rust codebase programmatically. Lastly, one could create similar bindings for another platform as long as a WASM interpreter is available.

Cross Module Calls

The WASM Plugin Handle architecture permits one WASM plugin to call into another. This is incredibly powerful. What this enables one to do is to package Sapio contracts that are generic and can call one another either by hash (with effective subresource integrity) or by a nickname (providing easy user customizability).

For example, suppose I was writing a standard contract component C which I publish. Then later, I develop a contract B which is designed to work with C. Rather than having to depend on C’s source code (which I may not want to do for various reasons), I could simply hard code C’s hash into B and call create_contract_by_key(key: &[u8; 32], args: Value, amt: Amount) to get the desired code. The plugin management system automatically searches for a contract plugin with that hash, and tries to call it with the provided JSON arguments. Using create_contract(key:&str, args:Value: amt:Amount), a nickname can be provided in which case the appropriate plugin is resolved by the environment.

#![allow(unused)]
fn main() {
struct C;
const DEPENDS_ON_MODULE : [u8; 32] = [0;32];
impl Contract for C {
    #[then]
    fn demo(self, ctx: Context) {
        let amt = ctx.funds()/2;
        ctx.template()
            .add_output(amt, &create_contract("users_cold_storage", /**/, amt), None)?
            .add_output(amt, &create_contract(&DEPENDS_ON_MODULE, /**/, amt), None)?
            .into()
    }
}
}

Typed Calls

SapioHostAPI<T, R> resolves a module locator to a key and provides typed calls. Arguments T implement Serialize, JsonSchema, and Clone; results R implement Deserialize and JsonSchema. Resolving the locator makes no claim that every value of T is accepted by that module.

Plugin APIs explicitly generate JSON Schema Draft 7 for both arguments and results. The host accepts that dialect and applies bounded, offline validation. The sapio-jsonschema fork retains the schemars package name and derive attributes, with opt-in implementations for Bitcoin and Miniscript types.

The native host validates each actual CreateArgs<T> input against the module’s advertised input schema before calling its create function. It validates each successful result against the advertised output schema before returning it, then the caller deserializes that result as R. Ordinary module errors remain errors. These checks enforce JSON constraints for that call; contract behavior and compatibility between whole interfaces require their own specifications.

Versioned enum variants identify shared calling conventions. For example, the batching interface defines its arguments as follows:

#![allow(unused)]
fn main() {
/// A payment to a specific address
#[derive(JsonSchema, Serialize, Deserialize, Clone)]
pub struct Payment {
    /// The amount to send
    #[serde(with = "bitcoin::amount::serde::as_btc")]
    #[schemars(with = "f64")]
    pub amount: bitcoin::Amount,
    /// # Address
    /// The Address to send to
    pub address: bitcoin::Address<bitcoin::address::NetworkUnchecked>,
}
#[derive(Serialize, JsonSchema, Deserialize, Clone)]
pub struct BatchingTraitVersion0_1_1 {
    pub payments: Vec<Payment>,
    #[serde(with = "bitcoin::amount::serde::as_sat")]
    #[schemars(with = "u64")]
    pub feerate_per_byte: bitcoin::Amount,
}
}

The payment amount deliberately uses bitcoin units through as_btc; the fee rate uses integer satoshis through as_sat. Upstream Amount defaults to integer-satoshi JSON, and the explicit schema annotations describe this interface’s chosen representation.

Addresses received from JSON use Address<NetworkUnchecked> and a string schema. A receiving contract must check the address against its compilation network before using it as an output, for example:

#![allow(unused)]
fn main() {
let address = payment.address.clone().require_network(ctx.network)?;
let destination = Compiled::from_address(address, bitcoin::Amount::ZERO);
}

The string schema describes the wire format; parsing and require_network enforce address validity and the selected network. Already checked Address values are used after that boundary.

The shared interface wraps those arguments in a versioned variant:

#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
pub enum Versions {
    BatchingTraitVersion0_1_1(BatchingTraitVersion0_1_1),
}

pub type BatchingModule = ContractModule<Versions>;
}

ContractModule<Versions> is a SapioHostAPI whose result is a compiled contract. The serialized arguments keep the version tag:

{"BatchingTraitVersion0_1_1":{"payments":[],"feerate_per_byte":0}}

The treepay example accepts this variant alongside its direct TreePay and Advanced variants. A receiver can therefore offer additional calling conventions while accepting the shared batching input. Its schema is checked against the actual call rather than compared for equality with the caller’s schema.

Future Work on Cross Module Calls

  • Gitian Packaging: Using a gitian signed packaging distribution system would enable a user to set up a web-of-trust setting for their sapio compiler and enable fetching of sub-resources by hash if they’ve been signed by the appropriate parties.
  • NameSpace Registration: A system to allow people to register names unambiguously would aid in ensuring no conflicts. For now, we can handle this using a centralized repo.
  • Remote CMC: In some cases, we may want to make a call to a remote server that will call a given module for us. This might be desirable if the server holds sensitive material that we shouldn’t have.

Rust Lib/Bin

There’s not much to be said here. Sapio code is just Rust code, so it can be shipped as a standalone rust library or binary tool.

This code can then be integrated into any codebase either natively or using FFI.

It’s a good idea to always package contracts as a library separate from the binary, so that if a user wants to natively incorporate the contract it is easy to do, and the packaged WASM or binary can be a utility based on it.

Sapio Studio

Sapio Studio is an in-development graphical user interface for Sapio.

Currently, Sapio Studio works based on managing a WASM plugin directory, so that users can more readily add contracts of their choosing.

Contracts packaged for WASM have some additional constraints or functionality for aiding in the generation of a UX.

Sapio Command Line Interface (CLI)

The Sapio CLI (or sapio-cli) is rapidly changing, but it is self documenting using cargo run sapio-cli help.

sapio-cli aids users in:

  1. compiling sapio contracts into templates
  2. binding compiled templates to specific utxos from your bitcoin wallet
  3. inspecting contract plugins
  4. running emulator servers

sapio-cli has a config file (location dependent on platform, under org.judica.sapio-cli e.g. /home/<usr>/.config/sapio-cli/config.json). The config file can be overriden with the -c flag. This file allows users to set parameters for compilation around:

  1. to use regtest/mainnet/signet/etc
  2. bitcoind to connect to & auth
  3. CTV emulator servers to use
  4. key-value mapping of nicknames to WASM plugin hashes.

Advanced Rust Patterns

Say it with me – Sapio’s Just Rust ™. Even though there’s a lot of additional paradigms and information to take in to use Sapio over normal Rust programming, at the end of the day you can integrate Sapio into any Rust paradigm you like.

That said, this section has a few useful patterns that merit specific mention as you may find yourself reaching for them again and again.

Type-level state machines

Rust types can express which actions a contract implementation provides. An optional action factory returns None when a transition is unavailable; this is separate from an action returning an empty set of transactions.

The following sketch uses Opened and Closed as state tags. Both states retain the owner’s independent spending policy, while only the open state exports the committed close transition.

#![allow(unused)]
fn main() {
use std::marker::PhantomData;

struct Opened;
struct Closed;
struct StatefulContract<State> {
    owner: bitcoin::XOnlyPublicKey,
    state: PhantomData<State>,
}

trait Moves: Sized + 'static {
    sapio::decl_then! { close }
}

impl Moves for StatefulContract<Opened> {
    #[sapio::then]
    fn close(self, ctx: Context) {
        let amount = ctx.funds();
        ctx.template()
            .add_output(amount, &StatefulContract::<Closed> {
                owner: self.owner,
                state: PhantomData,
            }, None)?
            .into()
    }
}

impl Moves for StatefulContract<Closed> {}

#[sapio::contract(actions(Self::close))]
impl<State: 'static> StatefulContract<State>
where
    Self: Moves,
{
    #[spend]
    fn owner(&self) -> Clause {
        Clause::Key(self.owner)
    }
}
}

decl_then! supplies an absent default factory. The open implementation replaces it; the closed implementation keeps it absent. The contract macro explicitly exports that optional interface and the independent owner policy.

For availability that depends on a value rather than a Rust type, use a #[condition] method with compile_if(...). For example, an eltoo state can omit its update action after reaching the maximum state number. Required and Nullable still distinguish an action that must generate a transaction from one that may legitimately return none.

Rust enums, traits, generics and const generics can organize more elaborate state machines. They execute while constructing and compiling contracts. An ordinary Rust state check is not automatically enforced by a spending script: the committed transition or fixed policy/evaluator must enforce the corresponding on-chain rule.

TryFrom Constructors

Often times we want to assure that various properties must be true about the arguments passed to a contract instance.

By using TryFrom and being careful with the visibility of inner fields it is possible to guarantee that the only way to get an X is by going through type Y.

This can be bound using the serde(try_from) attribute, which makes it so that any deserialization of X first passes through Y. This is particularly useful when X contains types (such as function pointers or caches) that cannot be deserialized, but we want to provide a way for a third party to pass JSON args to construct an X.

#![allow(unused)]
fn main() {
use std::convert::TryFrom;
use std::convert::TryInto;
use serde::*;
/// inner argument not pub, X cannot be constructed without going through Y
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(try_from="Y")]
pub struct X(u32);

#[derive(Serialize, Deserialize, JsonSchema)]
pub struct Y(pub u32);
impl TryFrom<Y> for X {
    type Error = &'static str;
    fn try_from(y: Y) -> Result<Self, Self::Error> {
        if y.0 < 10 {
            Err("Too Small I Guess?")
        } else {
            Ok(X(y.0))
        }
    }
}

let x: X = Y(10).try_into().unwrap();

}

Concrete & Generic Types

Generics

Often time, it can be useful to make a generic contract, such as:

#![allow(unused)]
fn main() {
struct GenericA {
    send_to: Box<dyn Compilable>
}
}

or

#![allow(unused)]
fn main() {
struct GenericB<T:Compilable> {
    send_to: T
}
}

In GenericA we use a trait object to allow us to let the send_to field equal any Compilable type while having the same type GenericA, whereas GenericB takes a type parameter that makes the GenericB more specifically typed.

To highlight the differences between the approaches, suppose I had a parent contract:

#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize, JsonSchema)]
struct ConcreteA;
#[derive(Serialize, Deserialize, JsonSchema)]
struct ConcreteB;
struct AliceAndBobFree {
    alice: GenericA;
    bob: GenericA;
}
/// inner types can differ
let example_free_ok = AliceAndBobFree { alice: GenericA{send_to: Box::new(ConcreteA)},
                                        bob: GenericA{send_to:Box::new(ConcreteB)}};

struct AliceAndBobRestricted<T> {
    alice: GenericB<T>;
    bob: GenericB<T>;
}

/// inner types cannot differ
let example_restricted_fails = AliceAndBobRestricted { alice: GenericB{send_to: ConcreteA},
                                                       bob: GenericB{send_to: ConcreteB}};
}

It might seem like you always want to use the GenericA variant, but there are cases where you might want to guarantee that Alice and Bob’s supplied contracts are the same type.

Concrete Wrappers

When you do have a generic type (either with trait objects or otherwise) it can be difficult to use across an application boundary. To get around this, one can create a wrapper type (or enum) that uses the TryFrom paradigm to provide paths for the type to be concrete. E.g.,

#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize, JsonSchema)]
enum Concrete {
    A(ConcreteA),
    B(ConcreteB),
}

impl TryFrom<Concrete> for GenericA {
    type Error = &'static str;

    fn try_from(concrete:Concrete) -> Result<Self, Self::Error> {
        match concrete {
            Concrete::A(a) => GenericA(Box::new(a)),
            Concrete::B(b) => GenericA(Box::new(b))
        }
    }
}
}

Thus a Concrete can be used in a Serialize/Deserialize/JsonSchema API bound context, whereas a GenericA could not.

TODO: Implement path for making this section easier!