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

QuickStart:

Sapio should work on all platforms, but is recommend 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 by running the below command in your terminal:
rustup target add wasm32-unknown-unknown
  1. Get the wasm-pack tool.

Tip: On an M1 Mac you may need to do the following:

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

and then load the following before compiling

export PATH="/opt/homebrew/opt/llvm/bin:$PATH"
export CC=/opt/homebrew/opt/llvm/bin/clang
export AR=/opt/homebrew/opt/llvm/bin/llvm-ar
rustup default nightly
  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/treepay/ && wasm-pack build && cd ..
  1. Instantiate a contract from the plugin:
cargo run --bin sapio-cli -- contract create \{\"amount\":9.99,\"arguments\":\{\"Basic\":\{\"fee_sats_per_tx\":1000,\"participants\":\[\{\"address\":\"bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw\",\"amount\":2.99\}\],\"radix\":2\}\},\"network\":\"Regtest\"\} --file="plugin-example/treepay/pkg/sapio_wasm_plugin_example_bg.wasm"

You can use cargo run --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 may have some errors, you will need to ensure you've configured your client correctly.

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.

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() {
use bitcoin::util::amount::CoinAmount;
use sapio::contract::*;
use sapio::*;
use sapio_base::timelocks::RelTime;
use sapio_base::Clause;
use std::convert::{TryFrom, TryInto};
pub struct TrustlessEscrow {
    alice: bitcoin::PublicKey,
    bob: bitcoin::PublicKey,
    alice_escrow: (CoinAmount, bitcoin::Address),
    bob_escrow: (CoinAmount, bitcoin::Address),
}

impl TrustlessEscrow {
    guard! {
        fn cooperate(self, ctx) {
            Clause::And(vec![Clause::Key(self.alice), Clause::Key(self.bob)])
        }
    }
    then! {
        fn use_escrow(self, ctx) {
            ctx.template()
                .add_output(
                    self.alice_escrow.0.try_into()?,
                    &Compiled::from_address(self.alice_escrow.1.clone(), None),
                    None)?
                .add_output(
                    self.bob_escrow.0.try_into()?,
                    &Compiled::from_address(self.bob_escrow.1.clone(), None),
                    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! {then, Self::use_escrow}
    declare! {non updatable}
}
}

Create a new rust project and paste the above code in. You should be able to compile it using cargo build.

Challenges

  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 avaialble in the online docs, which are more targetted 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 rust-miniscript which extends miniscript with functionality relevent 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.

Template Builder

The Template builder is one of the most important parts of a Sapio contract. It is how you define and build a transaction step.

It's also an area of active work to improve the UX of, to enable building new kinds of smart contract more easily, supporting more advanced constructs.

The below code demonstrates how to use the template builder. See the docs for more detail!


#![allow(unused)]
fn main() {
struct X;
impl X {
    then! {
        fn example(self, ctx) {
            /// create a new template with the current context
            /// and set lock time to height 100
            let mut tmpl = ctx.template().set_lock_time(AbsHeight::from(10).into())?;
            let h = vec![(String::from("Metadata"), String::from("IS_COOL"))].into_iter().collect();
            /// Add an output
            /// make sure to assign to update after initial assignment, otherwise tmpl is consumed completely...
            /// Note: What happens when X creates an X (infinite loop)
            tmpl = tmpl.add_output(bitcoin::Amount::from_sat(1000), &X, Some(h))?;
            /// mark some funds unavailable (e.g. fees)
            tmpl = tmpl.spend_amount(bitcoin::Amount::from_sat(0xFEE))?;
            /// note that tmpl has it's own clone of ctx, which we should be
            /// careful to use instead of the passed in ctx, which is immutable
            if tmpl.ctx().funds() < bitcoin::Amount::from_sat(100000) {
                return Err(CompilationError::TerminateCompilation);
            }
            /// certain metadata is inteded to be "non-proprietary" and has dedicated setters
            tmpl = tmpl.set_label("Example!".into());
            /// adds a new _input_ and sets it sequence to relheight 1 block.
            tmpl = tmpl.add_sequence().set_sequence(-1, RelHeight::from(1))?;
            /// add some additional funds (i.e. from the input we just added)
            tmpl = tmpl.add_amount(Bitcoin::from_sats(10000));
            /// Send the remaining funds to this output
            tmpl = tmpl.add_output(tmpl.ctx().funds(), &X, None)?;
            let feeling_lazy = true;
            if feeling_lazy {
                /// This finishes the builder and turns it into the correct result type
                tmpl.into()
            } else {
                /// equivalently, but more verbosely
                Ok(Box::new(std::iter::once(Template::from(tmpl))))
            }
        }
    }
}
impl Contract for X {
    /*...*/
}

}

The Sapio model currently expects that all contracts UTXO spends are located in the first input. The CTV hash commits to this, so it cannot be modified at this time (but future work might allow changing this).

Time Locks

Sapio provides some utilities for working with both relative and absolute timelocks. See the sapio-base docs for more details.

The Time Lock Utilities have some nice interfaces for dealing with timelocks generically and converting them into Policy Clauses.


#![allow(unused)]

fn main() {
use sapio_base::timelocks::*;
use std::time::Duration;

AbsHeight::try_from(800_000u32);
AbsTime::try_from(1_000_000_000u32);
AbsTime::try_from(Duration::from_secs(1_000_000_000u64));
// chunks of 512 seconds
RelTime::from(10u16);
RelTime::try_from(Duration::from_secs(10*512));
RelHeight::from(20u16);


// Correctly compiles into Clause::Older
let c: Clause = RelHeight::from(20u16).into();

let a: AnyRelTimeLock = RelHeight::from(20u16).into();
let b: AnyTimeLock = RelHeight::from(20u16).into();

}

These are not required to be used, but care should be taken if not used to ensure that correct values are passed to the miniscript compiler.

Sats and Coins

There are several different ways of expressing amounts in Sapio.

That there isn't a single canonical way to represent amounts is unfortunate, and hopefully these types can be fully unified in the future. But it's a problem for good reason.

A brief rant

Suppose I tell you to send 10 to Alice. Is that 10 sats? or 10 bitcoin? You might think that 10.0 would be unambiguous, but it turns out the lightning network is building sub-satoshi support.

The only way to make context-free unambiguous amounts is to have them explicityly tagged, e.g., {denom: "sats", amount: 10}.

This would be great, but there are already myriads of services out there where the only way to know what unit you have is to RTFM.

Generally, we know that floating point representations are evil for financial transactions, but because we want to be compatible with JSON/Javascript, we don't quite have a choice. Fortunately, 21e6 Bitcoin with 8 places fit exactly into floats without loss. However, bets are off when doing arithmetic with such values.

A last wrinkle: Bitcoin's amount type is a signed integer. Rust-bitcoin uses an Unsigned integer. So in theory there are unrepresentable amounts we're happy to work with. Great.

It's up to every programmer

Therefore, to get amounts right is a task that is up to the programmer largely to get this right. There are a few different amount types to be aware of.

  1. u64 represents sats. may be too big!
  2. i64 represents sats. may be too small!
  3. bitcoin::Amount represents u64, no standard serialization.
  4. bitcoin::SignedAmount represents i64, no standard serialization.
  5. bitcoin::CoinAmount standard tagged serialization, either u64 or f64.

These different types have uses in different circumstances.

Because bitcoin::Amount does not have a standard serializer, in order to use it in e.g. a Vec, you have to wrap the type with a a serializer. From impls can make life a little eaiser to work with these.


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

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// A wrapper around `bitcoin::Amount` to force it to serialize with f64.
#[derive(
    Serialize, Deserialize, JsonSchema, Clone, Copy, Debug, Ord, PartialOrd, PartialEq, Eq,
)]
#[serde(transparent)]
struct AmountF64(
    #[schemars(with = "f64")]
    #[serde(with = "bitcoin::util::amount::serde::as_btc")]
    Amount,
);

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

CoinAmount does not have this problem, but it can't be used in all contexts, e.g. extenral APIs that aren't tagged.

Don't Panic (or do)

A final annoyance is that bitcoin::Amount has arithmetic that may panic (unless you use the checked_ variants). So one must be careful to ensure that any set of values passed in are safe to add.

Sapio currently does not do a fantastic job of this, but that can be improved in the future.

Contract Actions

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

namefunction
guard!Create a clause using miniscript with access to the contract's values and context.
compile_if!Determine if a then! or finish! should be compiled based on the contract's values and context
then!Create a path or paths that are guaranteed using CTV for a contract to be spent, with optional guard!s and compile_if!s.
finish!Create a suggested path or paths for a contract to be spent that are not guaranteed via CTV with mandatory guard!s and compile_if!s. Also accepts an update argument for generating transactions based on future data.

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

Guard

Guards are central to any Sapio contract. They allow declaring a piece of miniscript logic.

These guards can either be used standalone as unlocking conditions or as a requirement on a finish! or then! function.

If a guard! is marked as cached, the compiler will make an effort to only invoke the guard! once during compilation. This is helpful in contexts where a guard! might be expensive to call, e.g. if it is programmed to retrieve a Clause from a remote server. It is not guaranteed that the guard! is only invoked once.

guard! macro


#![allow(unused)]
fn main() {
guard!{
    fn name(self, ctx) {/*Clause*/}
}
/// The guard should only be invoked once by the compiler, and the result stored
guard!{
    cached fn name(self, ctx) {/*Clause*/}
}
}

ConditionallyCompileIf

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() {
compile_if!{
    fn not_too_much(self, ctx) {
        if ctx.funds() > Self::MAX_FUNDS {
            ConditionallyCompileType::Never
        } else {
            ConditionalCompileType::NoConstraint
        }
    }
}
}

and apply it to the relevent paths.

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
}

compile_if! macro

The compile_if macro can be called two ways:


#![allow(unused)]
fn main() {
compile_if!{
    fn name(self, ctx) {
        /*ConditionalCompileType*/
    }
}
/// null implementation
compile_if!{name}
}

ThenFunc

A ThenFunc is a continuation of a contract that can proceed when all the guarded_by conditions on that object are met. The ThenFunc provides an iterator of possible next transactions, using CTV to ensure execution.

When to use a ThenFunc

We've already seen an example of a then! function in the wild in Chapter 1. In that example we are guaranteeing that after a timeout, a specific "return policy" is honored out of the escrow. Unless Alice and Bob agree to something else, the funds can only be returned via that transaction.

In general, any time you want a state transition to be "locked in" you should use a then!.

then! macro

The then! macro generates a static fn() -> Option<ThenFunc> method for a given impl.

There are a few variants of how you can create a then!.


#![allow(unused)]
fn main() {
/// A Guarded CTV Function
then!{
    guarded_by: [guard_1, ... guard_n]
    fn name(self, ctx) {
        /*Result<Box<Iterator<TransactionTemplate>>>*/
    }
}
/// A Conditional CTV Function
then!{
    compile_if: [compile_if_1, ... compile_if_n]
    fn name(self, ctx) {
        /*Result<Box<Iterator<TransactionTemplate>>>*/
    }
}
/// A Conditional + Guarded CTV Function
then!{
    compile_if: [compile_if_1, ... compile_if_n]
    guarded_by: [guard_1, ... guard_n]
    fn name(self, ctx) {
        /*Result<Box<Iterator<TransactionTemplate>>>*/
    }
}
/// An Unguarded CTV Function
then!{
    fn name(self, ctx) {
        /*Result<Box<Iterator<TransactionTemplate>>>*/
    }
}
/// Null Implementation
then!{name}
}

The Iterator must not be empty, or it will cause an error.

FinishOrFunc

A FinishOrFunc is a continuation of a contract that may terminate when all the guarded_by conditions on that object are met, but provides logic for some default continuations and logic for new continuations in light of new information.

FinishOrFuncs do not use CTV to ensure execution.

When to use a FinishOrFunc

An example of where a FinishOrFunc could be used is a multisig escrow contract, where if n-of-n interested parties agree to move the funds, the funds can move to any transaction. However, perhaps the escrow operators typically emit a payment to a third party and carry the remaining balances to a new escrow. A FinishOrFunc can provide convenient logic shared by all participants for generating what that next transaction should look like.

finish! macro

The finish! macro generates a static fn() -> Option<FinishOrFunc> method for a given impl.

There are a few variants of how you can create a finish!.


#![allow(unused)]
fn main() {
/// A Guarded CTV Function
finish!{
    guarded_by: [guard_1, ... guard_n]
    fn name(self, ctx, o) {
        /*Result<Box<Iterator<TransactionTemplate>>>*/
    }
}
/// A Conditional CTV Function
finish!{
    compile_if: [compile_if_1, ... compile_if_n]
    guarded_by: [guard_1, ... guard_n]
    fn name(self, ctx, o) {
        /*Result<Box<Iterator<TransactionTemplate>>>*/
    }
}
/// Null Implementation
finish!(name);
}

The type of the parameter o is Option<<Self as Contract>::StatefulArguments> and is the same across all FinishOrFuncs. Enums may be used to pass different arguments to different functions.

When to use macros?

Generally, you want to use finish!, then!, etc to generate your methods. However, if you prefer to create them manually, it's entirely possible to do so without much effort. A tool like cargo expand may be useful as you can just copy the macro output and customize from there.

One reason you might choose to manually define them is if you want to have custom static logic (that is, known just from the type and not a value-filled instance) to decide if a method should be Some or None. If it does not need to be static logic, a compile_if can be used.

Contract Declarations

Static Contracts

This is the usual way to declare a contract for Sapio.

Once a contract and all relevant logic has been defined, a impl Contract should be written. This binds the functionality to the compiler interface.


#![allow(unused)]
fn main() {
impl Contract for T {
    declare!{then, Self::a, Self::b}
    declare!{finish, Self::guard_1, Self::guard_2}
    /// if there are finish! functions
    declare!{updatable<Z>, Self::updatable_1}
    /// if there are no updatable functions
    declare!{non updatable}
}
}

The type Z above becomes bound for the updatable functions.

Dynamic Contracts

Sapio also supports several "Dynamic Contract" paradigms which allows a user to assemble contracts at run-time. The two main paradigms are accomplished by either directly impl AnyContract or by using the DynamicContract struct which holds all functions in vecs.

These are useful in rare circumstances.

External Addresses?

The compiler is able to "lift" an address or a script into a contract via Object::from_address and Object::from_script. Care should be taken when doing so as Sapio will not be able to provide any further API data beyond such a bound.

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.

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.

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) {
            Ok(Box::new(std::iter::once(
                ctx.template()
                    // set the timeout for this path -- because it is using
                    // then! we do not require a 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(),
            )))
        }
    }

    then! {
        /// continue the contract
        fn strikes(self, ctx) {
            let mut tmpl = ctx.template().add_amount(self.strike_price);
            tmpl.add_sequence()
                .add_output(
                    /// use the inner context of tmpl because it has added funds
                    (tmpl.ctx().funds() + self.strike_price).into(),
                    &self.strike_into
                    None,
                )?
                .into()
        }
    }
}

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

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. 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.
  2. 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.
  3. 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.
  4. Modify the contract so that expire_A is only present the fields required by it are Option::is_some (hint: use compile_if!).
  5. Add logic to deduct fees.
  6. 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.

The Default Emulator

Sapio CTV Emulators defines implementations of a local emulator that can be used by sapio compiler library users. To use such an emulator, a user can generate a seed and create a contract. After creating the contract and binding it to a specific UTXO, a user should be able to delete the seed, ensuring that only the compiled logic may be used. Alternatively, they can retain the seed and promise not to improperly use it.

This crate also defines logic for servers that want to offer emulator services to remote compilers. 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 a emulator server. Then the contract can be "immutable" unless both collude.

To aid in experimentation, Judica, Inc operates a public emulator server for regtest.

[
    "tpubD6NzVbkrYhZ4Wf398td3H8YhWBsXx9Sxa4W3cQWkNW3N3DHSNB2qtPoUMXrA6JNaPxodQfRpoZNE5tGM9iZ4xfUEFRJEJvfs8W5paUagYCE",
    "ctv.d31373.org:8367"
]

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 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

This emulator trait crate is a base that exports a trait definition and some helper structs that are needed across the sapio ecosystem.

Defining the trait in its own crate allows us to use trait objects in our compiler internals without needing to have the compiler directly depend on e.g. networking primitives.

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

Currently Taproot is scheduled to active on Bitcoin in November 2021 but there is limited support for it across all ecosystem tools.

Sapio scripts can become very large in size, and would greatly benefit from being able to split up and merkelize the logic into smaller satisfiable chunks. This makes it economical to use Sapio.

The compiler is currently relatively naive about this, and unknown (or worse, unchecked) errors might occur as a result of pushing these limits. Hopefully, rust-miniscript should catch such errors, but a malicious author might be able to trigger an unknown unsatisfiable script.

Without full Taproot support, Sapio is probably ill-advisable to use at writing, but this will hopefully change in the immediate future.

Taproot Optimizations

With Taproot comes the opportunity to Huffman Code spending paths to decrease fees even further. Sapio currently uses rust-miniscript Policy language to generate spending conditions, so Sapio should be able to carry metadata from the programmer about the likelihood of various paths being taken, but this currently only is used within a script as opposed to the Tapscript tree itself.

Advanced Transaction Handling

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

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. It also makes it relatively safe to run smart contracts provided by untrusted parties as the security of the WASM sandbox prevents bad code from harming or infecting our system.

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) {
            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()
        }
    }
}
}

Future Work on Cross Module Calls

  • Type System: Using JSONSchemas, plugins have a basic type system that enables run-time checking for compatibility. Work could be done to establish a trait based type system that can allow plugins to guarantee they implement particular interfaces faithfully. For example, users_cold_storage key could be wrapped in a type safe wrapper that knows how to respond to a ColdStorageArgs struct.
  • 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.
  • Concrete CMC: currently, CMC's only return the Compiled type. Perhaps future CMC support can return arbitrary types, allowing other types of functionality to be packaged.

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 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

In this example we use type level state machines to encode functionality that is potentially available. See the example below for a sketch of how this can work.


#![allow(unused)]
fn main() {
/// The contract we're building, that can be in any type-state T.
struct StatefulContract<T>(PhantomData<T>);

/// We use empty structs as type tags.
/// Note: we could add a `trait State`, but it is not required
/// 
/// A contract can be in the open state or the closed state.
struct Opened;
struct Closed;

/// The "state machine" defines functionality that may be available
trait FunctionalityAtState 
where Self : Sized + Contract
{
    /// empty declaration *could* be a default implementation, but we leave it empty
    /// so that other states may override it.
    then!{do_something}
}


/// Override the impl when state is Opened
impl FunctionalityAtState for StatefulContract<Opened> {
    then! {
        /// Transition from Opened => Closed state
        fn do_something(self, ctx) {
            ctx.template()
               .add_output(ctx.funds(),
                           &StatefulContract::<Closed>(Default::default()),
                           None)?.into()
        }
    }
}

/// do not override `do_something`, no branch will be generated
impl FunctionalityAtState for StatefulContract<Closed> {}

/// Register that all StatefulContract<T>'s that implement FunctionalityAtState
/// are Contracts
impl Contract for StatefulContract<T>
where Self : FunctionalityAtState {
    declare!{then, Self::do_something}
}
}

This technique is ridiculously powerful. Imagine, for instance, that we wanted to have different sorts of state other than Open and Closed. E.g., Red and Green. We could then define Transition Rules that encode a graph like:

(Open, Green) ==> do_something ==> (Closed, Green)
(Open, Red) ==> do_something ==> (Closed, Red)
(Open, Green) ==> do_something_else ==>  (Open, Red)
(Open, Red) ==> do_something_else ==> (Closed, Red)

using two separate FunctionalityAtState like traits:


#![allow(unused)]
fn main() {
/// The contract we're building, that can be in any type-state T.
struct StatefulContract<T1, T2>(PhantomData<(T1, T2)>);

/// We use empty structs as type tags.
/// Note: we could add a `trait State`, but it is not required
/// 
/// A contract can be in the open state or the closed state.
struct Opened;
struct Closed;
// And Red or Green
struct Red;
struct Green;

/// The "state machine" defines functionality that may be available
trait OpenAtState 
where Self : Sized + Contract
{
    /// empty declaration *could* be a default implementation, but we leave it empty
    /// so that other states may override it.
    then!{do_something}
}

trait ColorAtState 
where Self : Sized + Contract
{
    /// empty declaration *could* be a default implementation, but we leave it empty
    /// so that other states may override it.
    then!{do_something}
}


/// Override the impl when state is Opened
impl OpenAtState<DontCare> for StatefulContract<Opened, DontCare> {
    then! {
        /// Transition from Opened => Closed state
        fn do_something(self, ctx) {
            ctx.template()
               .add_output(ctx.funds(),
                           &StatefulContract::<Closed, DontCare>(Default::default()),
                           None)?.into()
        }
    }
}

/// do not override `do_something`, no branch will be generated
impl OpenAtState<DontCare> for StatefulContract<Closed, DontCare> {}

/// Override the impl when state is Opened
impl ColorAtState for StatefulContract<Open, Green> {
    then! {
        /// Transition from Green => Red state
        fn do_something_else(self, ctx) {
            ctx.template()
               .add_output(ctx.funds(),
                           &StatefulContract::<Open, Red>(Default::default()),
                           None)?.into()
        }
    }
}

impl ColorAtState for StatefulContract<Open, Red> {
    then! {
        /// Transition from Open => Closed state
        fn do_something_else(self, ctx) {
            ctx.template()
               .add_output(ctx.funds(),
                           &StatefulContract::<Closed, Red>(Default::default()),
                           None)?.into()
        }
    }
}

/// do not override `do_something_else`, no branch will be generated
impl ColorAtState<DontCare> for StatefulContract<DontCare, Red> {}

/// Register that all StatefulContract<T>'s that implement OpenAtState
/// are Contracts
impl Contract for StatefulContract<T>
where Self : OpenAtState + ColorAtState {
    declare!{then, Self::do_something, Self::do_something_else}
}
}

This technique showcases how Sapio could encode very sophisticated logic in program generation.

It's also notable that following rustc v1.51, it is possible to use const's as generic type parameters which enables even more computation at the type level.

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 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!