Designing Bitcoin Contracts with Sapio
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:
Today, Sapio can come to you in an easy to set up Docker compatible container (unofficialâ„¢). With the Sapio pod you get:
- A CTV Compatible Bitcoin Node running regtest
- Rust
- A pre-built cached Sapio Directory for you to use as a workspace
- sapio-cli pre-built
- Sapio Studio built and running over X11 connected to your regtest node
- 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.
- Get rust if you don't have it already.
- 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
- 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.
- 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
)
- 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\":1,\"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.
- 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() { #[derive(JsonSchema, Deserialize)] pub struct TrustlessEscrow { alice: bitcoin::PublicKey, bob: bitcoin::PublicKey, alice_escrow_address: bitcoin::Address, alice_escrow_amount: CoinAmount, bob_escrow_address: bitcoin::Address, bob_escrow_amount: CoinAmount, } impl TrustlessEscrow { #[guard] fn cooperate(self, _ctx: Context) { Clause::And(vec![Clause::Key(self.alice), Clause::Key(self.bob)]) } #[then] fn use_escrow(self, ctx: Context) { ctx.template() .add_output( self.alice_escrow_amount.try_into()?, &Compiled::from_address(self.alice_escrow_address.clone(), None), None, )? .add_output( self.bob_escrow_amount.try_into()?, &Compiled::from_address(self.bob_escrow_address.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} } REGISTER![TrustlessEscrow, "logo.png"]; }
Navigate to sapio/plugin-example/helloworld/plugin.rs
in your code editor.
You'll find this code there. You should be able to compile it using
cargo build --target wasm32-unknown-unknown
.
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.
- Add a new finish state that allows Alice to spend after a relative timeout.
- 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:
- nVersion
- nLockTime
- scriptSig hash (maybe!)
- input count
- sequences hash
- output count
- outputs hash
- 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:
- Alice and Bob Agree \( \rightarrow \) coin goes anywhere
- 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:
- Alice and Bob Agree \( \rightarrow \) coin goes anywhere
- 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\):
- Alice and Bob Agree \( \rightarrow \) coin goes anywhere
- Alice or Bob Requested \( \rightarrow \) (\(S \gets Closing \))
-
\( S \gets Closing\):
- Alice and Bob Agree \( \rightarrow \) coin goes anywhere
- 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:
- Given a combination of spending conditions, finding the most economical script to implement it.
- 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).
- Given a script, find out what spending conditions it permits.
- Given a script and access to a sufficient set of private keys, construct a general satisfying witness for it.
- Given a script, be able to predict the cost of spending an output.
- 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.
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 contracts more easily, supporting more advanced constructs.
The below code demonstrates how to use the template builder. See the docs for more details!
#![allow(unused)] fn main() { struct X; impl X { #[then] fn example(self, ctx: Context) { /// 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(100).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 since Miniscript doesn't validate these strictly.
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 explicitly 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.
- u64 represents sats. may be too big!
- i64 represents sats. may be too small!
bitcoin::Amount
represents u64, no standard serialization.bitcoin::SignedAmount
represents i64, no standard serialization.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 serializer. From
impls can make life a little easier 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. external 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.
name | function |
---|---|
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. |
continuation | 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. |
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
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 continuation
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( /// optional: if the compiler should attempt to only call this guard one time (not guaranteed) cached)] fn name(self, ctx: Context) {/*Clause*/} decl_guard!{name}; }
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: Context) { if ctx.funds() > Self::MAX_FUNDS { ConditionallyCompileType::Never } else { ConditionalCompileType::NoConstraint } } }
and apply it to the relevant 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 decl_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 Conditional + Guarded CTV Function #[then( /// optional: only compile these branches if these compile_if statements permit compile_if= "[compile_if_1, ... compile_if_n]", /// optional: protect these branches with the conjunction (and) of these clauses guarded_by= "[guard_1, ... guard_n]" )] fn name(self, ctx) { /*Result<Box<Iterator<TransactionTemplate>>>*/ } /// Null Implementation decl_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.
FinishOrFunc
s 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 continuation
macro generates a static fn() -> Option<FinishOrFunc>
method for a given impl.
There are a few variants of how you can create a continuation
.
#![allow(unused)] fn main() { struct UpdateType; /// Helper fn default_coerce( k: <T as Contract>::StatefulArguments, ) -> Result<UpdateType, CompilationError> { Ok(k) } /// A Guarded CTV Function #[continuation( /// required: guards for the miniscript clauses required guarded_by = "[Self::guard_1,... Self::guard_n]", /// optional: Conditional compilation compile_if = "[Self::compile_if_1, ... Self::compile_if_n]", /// optional: Enables compiling this for a json callable continuation web_api, /// helper for coercing args for json api, could be arbitrary coerce_args = "default_coerce" )] fn name(self, ctx:Context, o:UpdateType) { /*Result<Box<Iterator<TransactionTemplate>>>*/ } /// Null Implementation decl_finish!(name); }
The parameter o
is either called directly or attempted to be coerced from the
higher level <Self as Contract>::StatefulArguments
which mus enum wrap the
arguments. This means that each continuation
can have a unique parameter type,
but also be represented as a trait object with a single type. Enums may be used
to pass different arguments to different functions.
When to use macros?
Generally, you want to use continuation
, then
, guard
, 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, or the static logic can go
inside a compile_if
.
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:
- reconsider your priorities
- repeat step 1
- 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.
Continuation Effects
Suppose we had the following bit of code in a contract's implementation:
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize)] struct PayToKey(bitcoin::PublicKey); /// Helper fn default_coerce( k: <T as Contract>::StatefulArguments, ) -> Result<PayToKey, CompilationError> { Ok(k) } /// A Guarded CTV Function #[continuation( /// required: guards for the miniscript clauses required guarded_by = "[Self::guard_1,... Self::guard_n]", web_api, /// helper for coercing args for json api, could be arbitrary coerce_args = "default_coerce" )] fn to_address(self, ctx:Context, o:PayToKey) { let amt = ctx.funds(); ctx.template().add_output(amt, &o.0, None)?.into() } }
When the to_address
function gets passed by the compiler, a unique pointer (an effect path) is
generated for it from the context object. This enabled sending it parameters
later in the future.
On creation of the context object a effects: Arc<MapEffectDB>
parameter is
available. This MapEffectDB
links the effect paths to a list of arguments
intended to be passed to this branch which can generate new contract transitions
intended to be signed off on by the guards to that path.
For example, consider a contract for a NFT (a provenance checkable certificate of ownership).
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize)] struct NFT(bitcoin::PublicKey); #[derive(Serialize, Deserialize)] struct Sale(bitcoin::PublicKey, AmountF64); /// Helper fn default_coerce( k: <T as Contract>::StatefulArguments, ) -> Result<Sale, CompilationError> { Ok(k) } impl NFT { #[guard] fn signed(self, ctx:Context) { Clause::Key(self.0) } #[continuation( guarded_by = "[Self::signed]", web_api, /// helper for coercing args for json api, could be arbitrary coerce_args = "default_coerce" )] fn make_sale(self, ctx:Context, o:Sale) { let amt = ctx.funds(); ctx.template() .add_amount(o.1) // Carry whatever funds in the UTXO to the buyer in // a new NFT .add_output(amt, &NFT(o.0), None)? // Pay the sale amount to the previous owner .add_output(amt, &self.0, None)? .into() } } impl Contract for NFT { declare!{updatable<Sale>, Self::make_sale} } }
The updates generated through make_sale
generate the transactions for a series
of sales. For example, imagine I start with a NFT(Bob)
.
I can recompile NFT(Bob)
with the context (not exactly the pointer, but just for example)
NFT(Bob)
with effects
{"0": {"make_sale": [["Alice", 10]]}}
Supposing Alice pays into the transaction, a future transaction could be:
{"0": {"make_sale": [["Alice", 10]]}, "1": {"make_sale": [["Carol", 11]]}}
Thus by starting with a known valid NFT (e.g., Bob being the original artist), the effects can regenerate a series of state transitions for verification of provenance.
Further, as effects at a given point are a set, there could be multiple in flight transitions. E.g.,
{"0": {"make_sale": [["Alice", 10], ["Eve", 10]]}}
represents Alice and Bob both having the ability to purchase the NFT for 10 BTC.
Challenges
- By using decreasing time locks, implement a dutch auction pitting two participants against each other.
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:
- If \(\tau_{now} > \tau_{timeout} \):
- send funds to return address
- If
strike_price
btc are added:- send funds +
strike_price
to strike_into contract
- send funds +
#![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 // 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() } /// 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!(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.
- Clear out your helloworld plugin and put this code in.
- 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 preimageA
orB
. - Modify the contract so that there is a
expire_A
and aexpire_B
path that go to different addresses, andexpire_A
requires a signature or hash reveal to be taken. - Modify the contract so that if
expire_A
is taken, a small payoutearly_exit_fee: bitcoin::Address
is made to aearly_exit : bitcoin::Address
. - Modify the contract so that
expire_A
is only present the fields required by it areOption::is_some
(hint: usecompile_if!
). - Add logic to deduct fees.
- 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 an 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 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:
- contract specification can occur without any online processes
- The server has no intelligent logic, all guarantees are structural.
- Server is completely stateless.
- Availability/malfeasance can be controlled for with multisig
- 1:1 functionality mapping to CTV
The downside of this approach to emulation is that:
- It is somewhat inefficient for scripts which have many branched possibilities.
- 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
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:
- All contract transitions pay a high enough minimum we expect to be able to get into the mempool in the future
- There are ways to inject "gas inputs" into the contract, if needed
- There are ways to spend "gas outputs" from the contract just for Child-Pays-For-Parent logic.
- 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: 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
Using JSONSchemas, plugins have a basic type system that enables run-time checking for compatibility. Plugins can guarantee they implement particular interfaces faithfully. These interfaces currently only support protecting the call, but make no assurances about the returned value or potential errors from the callee's implementation of the trait.
For example, suppose I want to be able to specify a provided module must
statisfy a calling convention for batching. I define the trait
BatchingTraitVersion0_1_1
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::util::amount::serde::as_btc")] #[schemars(with = "f64")] pub amount: bitcoin::util::amount::Amount, /// # Address /// The Address to send to pub address: bitcoin::Address, } #[derive(Serialize, JsonSchema, Deserialize, Clone)] pub struct BatchingTraitVersion0_1_1 { pub payments: Vec<Payment>, #[serde(with = "bitcoin::util::amount::serde::as_sat")] #[schemars(with = "u64")] pub feerate_per_byte: bitcoin::util::amount::Amount, } }
I can then turn this into a SapioJSONTrait by implementing the trait and providing an "example" function.
#![allow(unused)] fn main() { impl SapioJSONTrait for BatchingTraitVersion0_1_1 { /// required to implement fn get_example_for_api_checking() -> Value { #[derive(Serialize)] enum Versions { BatchingTraitVersion0_1_1(BatchingTraitVersion0_1_1), } serde_json::to_value(Versions::BatchingTraitVersion0_1_1( BatchingTraitVersion0_1_1 { payments: vec![], feerate_per_byte: bitcoin::util::amount::Amount::from_sat(0), }, )) .unwrap() } /// optionally, this method may be overridden directly for more advanced type checking. fn check_trait_implemented(api: &dyn SapioAPIHandle) -> bool { Self::check_trait_implemented_inner(api).is_ok() } } }
If a contract module can receive the example, then it is considered to have implemented the API. We can implement the receivers for a module as follows:
#![allow(unused)] fn main() { struct MockContract; /// # Different Calling Conventions to create a Treepay #[derive(Serialize, Deserialize, JsonSchema)] enum Versions { /// # Base Base(MockContract), /// # Batching Trait API BatchingTraitVersion0_1_1(BatchingTraitVersion0_1_1), } impl From<BatchingTraitVersion0_1_1> for MockContract { fn from(args: BatchingTraitVersion0_1_1) -> Self { MockContract } } impl From<Versions> for TreePay { fn from(v: Versions) -> TreePay { match v { Versions::Base(v) => v, Versions::BatchingTraitVersion0_1_1(v) => v.into(), } } } REGISTER![[MockContract, Versions], "logo.png"]; }
Now MockContract
can be called via the BatchingTraitVersion0_1_1
trait
interface.
Another module in the future need only have a field
SapioHostAPI<BatchingTraitVersion0_1_1>
. This type verifies at deserialize
time that the provided name or hash key implements the required interface(s).
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.
- Concrete CMC: currently, CMC's only return the
Compiled
type. Perhaps futureCMC
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 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:
- compiling sapio contracts into templates
- binding compiled templates to specific utxos from your bitcoin wallet
- inspecting contract plugins
- 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:
- to use regtest/mainnet/signet/etc
- bitcoind to connect to & auth
- CTV emulator servers to use
- 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. decl_then!{do_something} } /// Override the impl when state is Opened impl FunctionalityAtState for StatefulContract<Opened> { /// Transition from Opened => Closed state #[then] fn do_something(self, ctx: Context) { 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. delc_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. decl_then!{do_something} } /// Override the impl when state is Opened impl OpenAtState<DontCare> for StatefulContract<Opened, DontCare> { /// Transition from Opened => Closed state #[then] fn do_something(self, ctx: Context) { 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> { /// Transition from Green => Red state #[then] fn do_something_else(self, ctx: Context) { ctx.template() .add_output( ctx.funds(), &StatefulContract::<Open, Red>(Default::default()), None, )? .into() } } impl ColorAtState for StatefulContract<Open, Red> { /// Transition from Open => Closed state #[then] fn do_something_else(self, ctx: Context) { 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 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.