Composability in Sapio Contracts
Day 16: Rubin's Bitcoin Advent Calendar
TweetWelcome to day 16 of my Bitcoin Advent Calendar. You can see an index of all the posts here or subscribe at judica.org/join to get new posts in your inbox
Who here has some ERC-20s or 721s1? Anyone? No one? Whatever.
The Punchline is that a lotta fuss goes into Ethereum smart contracts being Turing Complete but guess what? Neither ERC-20 nor 721 really have anything to do with being Turing Complete. What they do have to do with is having a tightly defined interface that can integrate into other applications nicely.
This is great news for Bitcoin. It means that a lot of the cool stuff happening in eth-land isn’t really about Turing Completeness, it’s about just defining really kickass interfaces for the things we’re trying to do.
In the last few posts, we already saw examples of composability. We took a bunch of concepts and were able to nest them inside of each other to make Decentralized Coordination Free Mining Pools. But we can do a lot more with composability than just compose ideas togehter by hand. In this post I’ll give you a little sampler of different types of programmatic composability and interfaces, like the ERC-20 and 721.
Address Composability
Because many Sapio contracts can be made completely noninteractively (with CTV or an Oracle you’ll trust to be online later), if you compile a Sapio contract and get an address you can just plug it in somewhere and it “composes” and you can link it later. We saw this earlier with the ability to make a channel address and send it to an exchange.
However, for Sapio if you just do an Address it won’t necessarily have the understanding of what that address is for so you won’t get any of the Sapio “rich” features.
Pre-Compiled
You can also take not just an address, but an entire (json-serialized?) Compiled object that would include all the relevant metadata.
Rust Generic Types Composability
Well, if you’re a rust programmer this basically boils down to rust types rule! We’ll give a couple examples.
The simplest example is just composing directly in a function:
#[then]
fn some_function(self, ctx: Context) {
ctx.template()
.add_output(ctx.funds(), &SomeOtherContract{/**/}, None)?
.into()
}
What if we want to pass any Contract as an argument for a Contract? Simple:
struct X {
a : Box<dyn Contract>
}
What if we want to restrict it a little bit more? We can use a trait bound. Now only Y (or anything implementing GoodContract) can be plugged in.
trait GoodContract : Contract {
decl_then!{some_thing}
}
struct Y {
}
impl GoodContract for Y {
#[then]
fn some_thing(self, ctx: Context) {
empty()
}
}
impl Contract for Y {
declare!{then, Self::some_thing}
}
struct X<T: GoodContract> {
a : Box<dyn GoodContract>,
// note the inner type of a and b don't have to match
b : Box<dyn GoodContract>
}
Boxing gives us some power to be Generic at runtime, but we can also do some more “compile time” logic. This can have some advantages, e.g., if we want to guarantee that types are the same.
struct X<T : Contract, U: GoodContract> {
a : T,
b : T
// a more specific concrete type -- could be a T even
c: U,
d: U,
}
Sometimes it can be helpful to wrap things in functions, like we saw in the Vaults post.
struct X<T: Contract>
// This lets us stub in whatever we want for a function
a : Box<Fn(Self, Context) -> TxTmplIt>,
// this lets us get back any contract
b : Box<Fn(Self, Context) -> Box<dyn Contract>>,
// this lets us get back a specific contract
c : Box<Fn(Self, Context) -> T,
}
Clearly there’s a lot to do with the rust type system and making components.
It would even be possible to make certain types of ‘unchecked’ type traits, for example:
trait Reusable {}
struct AlsoReusable<T> {
a: T,
}
// Only reusable if T Reusable
impl<T> Reusable for AlsoReusable<T> where T: Reusable {}
The Reusable
tag could be used to tag contract components that would be “reuse
safe”. E.g., an HTLC or HTLC containing component would not be reuse safe since
hashes could be revealed. While reusability isn’t “proven” – that’s up to the
author to check – these types of traits can help us reason about the properties
of compositions of programs more safely. Unfortunately, Rust lacks negative
trait bounds (i.e., Not-Reusable), so you can’t reason about certain types of things.
Inheritence
We don’t have a fantastic way to do inheritence in Sapio presently. But stay
tuned! For now, then best you get is that you can do traits (like
GoodContract
).
Cross Module Composability & WASM
One of the goals of Sapio is to be able to create contract modules with a well-defined API Boundary that communicates with JSONs and is “typed” with JSONSchema. This means that the Sapio modules can be running anywhere (e.g., a remote server) and we can treat it like any other component.
Another goal of Sapio is to make it possible to compile modules into standalone WASM modules. WASM stands for Web Assembly, it’s basically a small deterministic computer emulator program format so we can compile our programs and run them anywhere that the WASM interpreter is available.
Combining these two goals, it’s possible for one Sapio program to dynamically load another as a WASM module. This means we can come up with a component, compile it, and then link to it later from somewhere else. For example, we could have a Payment Pool where we make each person’s leaf node a WASM module of their choice, that could be something like a Channel, a Vault, or anything that satisfies a “Payment Pool Payout Interface”.
For example, suppose we wanted to a generic API for making a batched payment.
Defining the Interface
First, we define a payment that we want to batch.
/// A payment to a specific address
pub struct Payment {
/// # Amount (btc)
/// The amount to send
pub amount: AmountF64,
/// # Address
/// The Address to send to
pub address: bitcoin::Address,
}
Next, we define the full API that we want. Naming and versioning is still a something we need to work on in the Sapio ecosystem, but for now it makes sense to be verbose and include a version.
pub struct BatchingTraitVersion0_1_1 {
pub payments: Vec<Payment>,
/// # Feerate (Bitcoin per byte)
pub feerate_per_byte: AmountF64
}
Lastly, to finish defining the API, we have to do something really gross looking
in order to make it automatically checkable – this is essentially this is what the
user defined BatchingTraitVersion0_1_1
is going to verify modules are able to
understand. This is going to be improved in Sapio over time for better typechecking!
impl SapioJSONTrait for BatchingTraitVersion0_1_1 {
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: Amount::from_sat(0).into(),
},
))
.unwrap()
}
}
Implementing the Interface
Let’s say that we want to make a contract like TreePay
implement
BatchingTraitVersion0_1_1
. What do we need to do?
First, let’s get the boring stuff out of the way, we need to make the TreePay
module understand that it should support BatchingTraitVersion0_1_1
.
/// # Different Calling Conventions to create a Treepay
enum Versions {
/// # Standard Tree Pay
TreePay(TreePay),
/// # Batching Trait API
BatchingTraitVersion0_1_1(BatchingTraitVersion0_1_1),
}
REGISTER![[TreePay, Versions], "logo.png"];
Next, we just need to define logic converting the data provided in
BatchingTraitVersion0_1_1
into a TreePay
. Since BatchingTraitVersion0_1_1
is really basic, we need to pick values for the other fields.
impl From<BatchingTraitVersion0_1_1> for TreePay {
fn from(args: BatchingTraitVersion0_1_1) -> Self {
TreePay {
participants: args.payments,
radix: 4,
// estimate fees to be 4 outputs and 1 input + change
fee_sats_per_tx: args.feerate_per_byte * ((4 * 41) + 41 + 10),
timelock_backpressure: None,
}
}
}
impl From<Versions> for TreePay {
fn from(v: Versions) -> TreePay {
match v {
Versions::TreePay(v) => v,
Versions::BatchingTraitVersion0_1_1(v) => v.into(),
}
}
}
Using the Interface
To use this BatchingTraitVersion0_1_1
, we can just define a struct as follows,
and when we deserialize it will be automatically verified to have declared a
fitting API.
pub struct RequiresABatch {
/// # Which Plugin to Use
/// Specify which contract plugin to call out to.
handle: SapioHostAPI<BatchingTraitVersion0_1_1>,
}
The SapioHostAPI
handle can be either a human readable name (like
“user_preferences.batching” or “org.judica.modules.batchpay.latest”) and looked
up locally, or it could be an exact hash of the specific module to use.
We can then use the handle to resolve and compile against the third party module. Because the module lives in an entirely separate WASM execution context, we don’t need to worry about it corrupting our module or being able to access information we don’t provide it.
Call to Action
ARE YOU A BIG BRAIN PROGRAMMING LANGUAGE PERSON?
PLEASE HELP ME MAKE THIS SAPIO HAVE A COOL AND USEFUL TYPE SYSTEM I AM A SMALL BRAIN BOI AND THIS STUFF IS HARD AND I NEED FRENZ.
EVEN THE KIND OF “FRENZ” THAT YOU HAVE TO PAY FOR wink.
In the posts coming Soon™, we’ll see some more specific examples of contracts that make heavier use of having interfaces and all the cool shit we can get done.
That’s all I have to say. See you tomorrow.
-
if you’ve been living under a big rock, ICO tokens and NFTs. ↩