This post is syndicated from rubin.io.
Welcome to day 21 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
Today’s a bit of a cheat day for me – not really “new” content, but mostly stuff re-packaged1 from learn.sapio-lang.org.
But it belongs in the series, and is it really plagarism if I wrote it myself?
How should you release it? How should you use it?
Today’s post 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 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. The determinism also enables our update system. 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:
/// 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.
The best way to make a new plugin is just to copy that directory and update the Cargo.toml
with a new name.
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 any rust codebase programmatically. Lastly, one could create
similar bindings for another platform as long as a WASM interpreter is
available.
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 – for example C
could be a standard), 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. Lastly, it’s possible to use
lookup_this_module_name()
to resolve the currently executing modules hash for
recursive calls. Recursive CMC calls can be helpful when you want to either
make a contract generic, or you want a clean JSON argument interface between
units. It’s also possible for a contract to detect if a generic argument
would result in a recursive CMC and cut-through it locally.
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_by_key(&DEPENDS_ON_MODULE, /**/, amt), None)?
.add_output(amt, &create_contract_by_key(&lookup_this_module_name().unwrap(), /**/, amt), None)?
.into()
}
}
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:
/// A payment to a specific address
#[derive(JsonSchema, Serialize, Deserialize, Clone)]
pub struct Payment {
/// The amount to send in sats
pub amount: AmountU64,
/// # Address
/// The Address to send to
pub address: Address,
}
#[derive(Serialize, JsonSchema, Deserialize, Clone)]
pub struct BatchingTraitVersion0_1_1 {
pub payments: Vec<Payment>,
pub feerate_per_byte: AmountU64,
}
I can then turn this into a SapioJSONTrait by implementing the trait and providing an “example” function.
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: Amount::from_sat(0).into(),
},
))
.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:
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).
Compiled
type. Perhaps
future CMC
support can return arbitrary types, allowing other types of
functionality to be packaged. For example, it would be great if a guard
clause
could be generated just from a separate WASM module.Well, ngmi. JK. Kinda.
You do really want WASM. You very much want your contracts to be deterministically compiled. If they are not, then a lot of things are not guaranteed to work correctly and you might lose funds.
We’re very focused on run-in WASM and not focused on other things.
That said, Sapio is just a Rust library, so you can embed your contracts into an application directly, e.g., for an embedded signing device.
If you do this it is paramount that you carefully audit and check that you are able to get consistent deterministic results out, or that you do not need to be able to deterministically recompile (this is true in many cases!) and can save the compilation result.
Another technique you can use is to build a bigger application around a contract and then compile that to a WASM blob. Also works fine if you’re careful not to accidentally add some entropy.
That’s all folks. In sum: Sapio is using WASM, you can choose to not use it at your own peril.
pun certainly intended. ↩︎