Here you'll find an assorted mix of content from yours truly. I post about a lot
of things, but primarily
Day 22: Rubin's Bitcoin Advent Calendar
19 Dec 2021
Welcome to day 22 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
I promised you a few things a few days ago:
- We’d see how to do royalties in a sale
- We’d see how to do a Dutch auction
- We’d see how to do batch mints
- We’d see how to make generative art
and one thing I didn’t
- In game items
Let’d get it done, son.
Royalties and Dutch Auction:
A Dutch Auction is a theoretically beautiful form on an auction that is great for sellers.
The way it works is that if you want to sell a piece, you start selling it at
price that you think no one could buy it for, and then slowly lower the price.
For example, suppose I have a car that the blue book value is $10,000 for. I
start by offerring it at $15,000k, and then drop it by $10 per second until
someone buys it. After about 10 minutes, the price will be $9,000, so a pretty
good deal. But before that, the price will be all prices between $9k and $15k.
So if a buyer thinks the car is actually a pretty good deal at $11k, and a great
deal at $10.5k, they would want to bid (assuming lots of bidders) at $11k lest
someone else buy it first.
Thus Dutch Auctions are very favorable to sellers, so natually, sellers like them.
Let’s patch our earlier NFT System to support
Dutch Auctions! While we’re at it let’s toss in royalties too!
First, we need to clean up a couple things about our NFT Definitions. These are
sorta trivial changes – really if I had planned better I’d have included them
from the get-go.
To our Minting trait we’re going to add a few fields:
- A key for the creator
- A ‘royalty’ percent (0 to disable)
/// # Trait for a Mintable NFT
#[derive(Serialize, JsonSchema, Deserialize, Clone)]
pub struct Mint_NFT_Trait_Version_0_1_0 {
/// # Creator Key
pub creator: bitcoin::PublicKey,
/// # Initial Owner
/// The key that will own this NFT
pub owner: bitcoin::PublicKey,
/// # Locator
/// A piece of information that will instruct us where the NFT can be
/// downloaded -- e.g. an IPFs Hash
pub locator: String,
/// # Minting Module
/// If a specific sub-module is to be used / known -- when in doubt, should
/// be None.
pub minting_module: Option<SapioHostAPI<Mint_NFT_Trait_Version_0_1_0>>,
/// how much royalty, should be paid, as a percent
pub royalty: f64,
}
Next, we’re going to add to our Sale trait a start time (e.g. blockheight).
/// # NFT Sale Trait
/// A trait for coordinating a sale of an NFT
#[derive(Serialize, JsonSchema, Deserialize, Clone)]
pub struct NFT_Sale_Trait_Version_0_1_0 {
/// # Owner
/// The key that will own this NFT
pub sell_to: bitcoin::PublicKey,
/// # Price
/// The price in Sats
pub price: AmountU64,
/// # NFT
/// The NFT's Current Info
pub data: Mint_NFT_Trait_Version_0_1_0,
/// # Sale Time
/// When the sale should be possible after
pub sale_time: AbsHeight,
/// # Extra Information
/// Extra information required by this contract, if any.
/// Must be Optional for consumer or typechecking will fail.
/// Usually None unless you know better!
pub extra: Option<Value>,
}
These fields could have gone into the extra data, but since it was probably a
mistake to not have them from the get-go we’ll allow it this time without increasing
our version numbers (nothings been released yet!).
Next, we’ll go ahead and create a new plugin module for our Dutch auction.
First we define some data that we have to have for a Dutch auction:
/// # Dutch Auction Data
/// Additional information required to initiate a dutch auction
#[derive(JsonSchema, Serialize, Deserialize)]
struct DutchAuctionData {
/// How often should we decreate the price, in blocks
period: u16,
/// what price should we start at?
start_price: AmountU64,
/// what price should we stop at?
min_price: AmountU64,
/// how many price decreases should we do?
updates: u64,
}
Then we define how to translate that into a schedule of sale prices:
impl DutchAuctionData {
/// # Create a Schedule for Sale
/// computes, based on a start time, the list of heights and prices
fn create_schedule(
&self,
start_height: AbsHeight,
) -> Result<Vec<(AbsHeight, AmountU64)>, CompilationError> {
let mut start: Amount = self.start_price.into();
let stop: Amount = self.min_price.into();
let inc = (start - stop) / self.updates;
let mut h: u32 = start_height.get();
let mut sched = vec![(start_height, self.start_price)];
for _ in 1..self.updates {
h += self.period as u32;
start -= inc;
sched.push((AbsHeight::try_from(h)?, start.into()));
}
Ok(sched)
}
Finally, we want to be able to derive this data with some default choices
in case a user wants to not select specific parameters. Hope you liked what we pick!
/// derives a default auction where the price drops every 6
/// blocks (1 time per hour), from 10x to 1x the sale price specified,
/// spanning a month of blocks.
fn derive_default(main: &NFT_Sale_Trait_Version_0_1_0) -> Self {
DutchAuctionData {
// every 6 blocks
period: 6,
start_price: (Amount::from(main.price) * 10u64).into(),
min_price: main.price,
// 144 blocks/day
updates: 144 * 30 / 6,
}
}
}
With the parameters for a Dutch Auction out of the way, now we can implement the
contract logic. First, the boring stuff:
#[derive(JsonSchema, Serialize, Deserialize)]
pub struct NFTDutchAuction {
/// This data can be specified directly, or default derived from main
extra: DutchAuctionData,
/// The main trait data
main: NFT_Sale_Trait_Version_0_1_0,
}
/// # Versions Trait Wrapper
#[derive(Serialize, Deserialize, JsonSchema)]
enum Versions {
/// Use the Actual Trait API
NFT_Sale_Trait_Version_0_1_0(NFT_Sale_Trait_Version_0_1_0),
/// Directly Specify the Data
Exact(DutchAuctionData, NFT_Sale_Trait_Version_0_1_0),
}
impl Contract for NFTDutchAuction {
declare! {updatable<()>, Self::transfer}
}
fn default_coerce<T>(_: T) -> Result<(), CompilationError> {
Ok(())
}
impl TryFrom<Versions> for NFTDutchAuction {
type Error = CompilationError;
fn try_from(v: Versions) -> Result<NFTDutchAuction, Self::Error> {
Ok(match v {
Versions::NFT_Sale_Trait_Version_0_1_0(main) => {
// attempt to get the data from the JSON:
// - if extra data, must deserialize
// - return any errors?
// - if no extra data, derive.
let extra = main
.extra
.clone()
.map(serde_json::from_value)
.transpose()
.map_err(|_| CompilationError::TerminateCompilation)?
.unwrap_or_else(|| DutchAuctionData::derive_default(&main));
NFTDutchAuction { main, extra }
}
Versions::Exact(extra, main) => {
if extra.start_price < extra.min_price || extra.period == 0 || extra.updates == 0{
// Nonsense
return Err(CompilationError::TerminateCompilation);
}
NFTDutchAuction { main, extra },
}
})
}
}
REGISTER![[NFTDutchAuction, Versions], "logo.png"];
Now, the fun part! Implementing it. This is basically the same as our NFTs from
the other day, but we just do sales along the schedule we generated:
impl NFTDutchAuction {
/// # signed
/// sales must be signed by the current owner
#[guard]
fn signed(self, ctx: Context) {
Clause::Key(self.main.data.owner.clone())
}
/// # transfer
/// transfer exchanges the NFT for cold hard Bitcoinz
#[continuation(guarded_by = "[Self::signed]", web_api, coerce_args = "default_coerce")]
fn transfer(self, base_ctx: Context, u: ()) {
let mut ret = vec![];
let schedule = self.extra.create_schedule(self.main.sale_time)?;
let mut base_ctx = base_ctx;
// the main difference is we iterate over the schedule here
for (nth, sched) in schedule.iter().enumerate() {
let ctx = base_ctx.derive_num(nth as u64)?;
let amt = ctx.funds();
// first, let's get the module that should be used to 're-mint' this NFT
// to the new owner
let key = self
.main
.data
.minting_module
.clone()
.ok_or(CompilationError::TerminateCompilation)?
.key;
// let's make a copy of the old nft metadata..
let mut mint_data = self.main.data.clone();
// and change the owner to the buyer
mint_data.owner = self.main.sell_to;
// let's now compile a new 'mint' of the NFT
let new_nft_contract = Ok(CreateArgs {
context: ContextualArguments {
amount: ctx.funds(),
network: ctx.network,
effects: unsafe { ctx.get_effects_internal() }.as_ref().clone(),
},
arguments: mint_impl::Versions::Mint_NFT_Trait_Version_0_1_0(mint_data),
})
.and_then(serde_json::to_value)
.map(|args| create_contract_by_key(&key, args, Amount::from_sat(0)))
.map_err(|_| CompilationError::TerminateCompilation)?
.ok_or(CompilationError::TerminateCompilation)?;
// Now for the magic:
// This is a transaction that creates at output 0 the new nft for the
// person, and must add another input that pays sufficiently to pay the
// prior owner an amount.
// todo: we also could use cut-through here once implemented
// todo: change seem problematic here? with a bit of work, we could handle it
// cleanly if the buyer identifys an output they are spending before requesting
// a purchase.
let price: Amount = sched.1.into();
ret.push(Ok(ctx
.template()
.add_output(amt, &new_nft_contract, None)?
.add_amount(price)
.add_sequence()
// Pay Sale to Seller
.add_output(
Amount::from_btc(price.as_btc() * (1.0 - self.main.data.royalty))?,
&self.main.data.owner,
None,
)?
// Pay Royalty to Creator
.add_output(
Amount::from_btc(price.as_btc() as f64 * self.main.data.royalty)?,
&self.main.data.creator,
None,
)?
// only active at the set time
.set_lock_time(sched.0.into())?
.into()))
}
Ok(Box::new(ret.into_iter()))
}
}
What’s interesting is that this contract is technically just a helper on-top of
our earlier Sale definition. Granted, we really ought to have had the royalty
and timelock before, but we could emulate a dutch auction by just calling the
regular Sale contract n times with different locktimes and prices. So we didn’t
really have to implement a standalone system for this. However, for more
advanced or bespoke things (like sales that also mint an NFT comemorating the
Sale itself) we’d want a bespoke module. Plus, the module makes it simple to
ensure that the type of auction and rate of change in price is well understood.
If desired, the DutchAuctionData
could also have different sorts of logic for
different price curves (e.g. Geometric, Linear, S-Curve, Custom).
Fun!
Abstract Client Verifier Auction
After an auction closes, in order for them to be able to prove to a future party
it was made correctly, they would need to run the identical Sapio code and
generate all possible execution price transactions.
This is not just computationally annoying, it’s also not very “lightweight”.
And it can lead to bugs like some bozo writing a contract which does not
do what it says it does (and pays no royalties).
An Abstract Client Verifier Auction could be set up as a postcondition on the
transactions generated by a Sale that they all be able to be re-generated by a
specialized template builder that just checks basic properties like “was a
royalty paid”.
We won’t go into detail on this here, but you could imagine patching Sell
as follows:
/// # Sell Instructions
#[derive(Serialize, Deserialize, JsonSchema)]
pub enum Sell {
/// # Hold
/// Don't transfer this NFT
Hold,
/// # MakeSale
/// Transfer this NFT
MakeSale {
/// # Which Sale Contract to use?
/// Specify a hash/name for a contract to generate the sale with.
which_sale: SapioHostAPI<NFT_Sale_Trait_Version_0_1_0>,
/// # The information needed to create the sale
sale_info: NFT_Sale_Trait_Version_0_1_0,
},
VerifySale {
txn: Bitcoin::Transaction
}
}
and the NFT can verify that the Sale transaction was valid according to it’s
choice of rule (or maybe even an artist selected Verifier module).
This might not be a huge deal / worth doing given that the Cross-Module-Call
results for client-side validation are cacheable.
Batch Mints
Batch mints are important because they allow an artist to fairly and easily
distribute their art. It’s really important for batch mints that the artist be
able to disseminate a single Output + Contract info and sign it per collection.
Even if the artist/their server has to be online to sell the work, users
should be able to unambiguously see who got which art.
Conceptually speaking – no code for now – Batch Mints can be done several
ways. It really depends what the artist wants:
Single Transaction
Do a single transaction whereby every minted NFT has an output.
- Simple!
- Big all at once cost bourne by artist
- No enforced “minting order”
Annuity of NFTs
Embed the mint contract into an Annuity where the successful auction of the ith NFT starts
the auction of the ith+1.
- Cheaper for the artist
- Requires the server be online
- Serial issuance piece i+1 can’t be bought till i is (buyers may clear i to get to i+1)
Congestion Control Tree of NFTs
- Cheaper for the artist
- Auctions can proceed independently for every piece
- Server is required.
Generative Art:
This concept is actually… pretty simple!
If you want to automatic generative art, essentially all you need to do is give
your NFT Contract (or your NFT Minting contract) some piece of state and a
function to convert the metadata description of the NFT + a pointer to the
transaction’s location and then you can generate a random seed for generating
that piece via your generate_art function.
struct MyNFT;
impl MyNFT {
fn generate_art(&self, b: BlockHash, offset: u64) -> String {
/*
Make your artz here
*/
}
}
This can be fun for things like creating the entropy for input to e.g. a machine
learning model.
Bonus: Updatable NFTs
Imagine you have a rare sword NFT for a videogame.
struct Sword {
sharpness: u64,
kills: u64
}
Every 10 kills you -1 sharpness, and every time you sharpen it you get +100 sharpness.
impl Sword {
#[continuation = "[Self::signed]"]
fn sharpen(self, ctx: Context, times: u64) {
/*
Pay 1000 sats to the game dev per time sharpened
*/
}
#[continuation = "[Self::signed]"]
fn register_kills(self, ctx: Context, headcount: u64) {
/*
update the metadata with a commitment to v
*/
}
}
These state transitions would be verified by anyone playing the game with you, using Bitcoin as the Database.
bbbbbuttt on-chain load
Not to sweat – simply build in to the continuation logic the ability to load in
an attestation chain (remember those?) of
lightning invoices of you paying the game developer over LN.
The attestation chain means that cheating would be duly punishable by loss of
bonds. You can also log things like ‘kills’ by publishing your game record through
the attestation chain with a signature from the other player you killed.
Any time you move or sell your NFT you can checkpoint into the metadata a copy
of the attestation chain “sealing” those actions. One tweak we can make to the
attestation chains is to require a regular “heartbeat” attestation from players
as well as a freeze attestation. This helps ensure that players buying an NFT
that they have all the latest state of the item loaded and other players can
check that there’s nothing missing.
galaxy brain: what if you bake into your NFT an attestation chain spec and the
thing you lose for lying is the item itself? And then you can do a special
HTLC-like contract whereby you have to prove you didn’t cheat for 2 weeks before
getting the payment from your counterparty, else they get a refund.
Overall I hope this post has opened your mind up wildly about the possibilities with Bitcoin NFTs…
I apologize I didn’t have more code ready and the post is late, but writing these posts is hard and
I’ve been focusing on the end of the series too :)
Day 21: Rubin's Bitcoin Advent Calendar
18 Dec 2021
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-packaged from learn.sapio-lang.org.
But it belongs in the series, and is it really plagarism if I wrote it myself?
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?
Today’s post covers various ways to deploy and use Sapio contracts.
Note on Open Sourcing:
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.
Packaging Contracts via 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. 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.
Cross Module Calls (CMC)
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()
}
}
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:
/// 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).
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.
- Polymorphic 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. For example, it would be great if a guard
clause
could be generated just from a separate WASM module.
What if I don’t want WASM?
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.
Day 20: Rubin's Bitcoin Advent Calendar
17 Dec 2021
Welcome to day 20 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 post is going to be a bit lighter weight than yesterday’s. We’ll cover
some high level concepts around oracles and then look at some Sapio.
The genesis of this line of inquiry was a conversation with Robin Linus that led to
a pretty cool whitepaper, so definitely
read that if you find this post compelling.
Oracles
Oracles are cool! The most basic form of an useful bitcoin oracle is just a
signing key that signs transactions or reveals information that it “should”
according to some rule.
Protocols for oracles like discrete log contract oracles produce more generic
“key material reveals”, that are more similar to releasing information that
allows counterparties to decrypt the relevant signature.
One of the problems with oracles is that they can equivocate, that is, sign
multiple conflicting statements. It would be nice if we could esnure that they
would be consistent, no?
Bonded Oracles
In order to make the oracles consistent, what we can do is set up our oracles
such that if the oracle ever signs two statements they reveal their private key
to the world. The common way that this is done is via nonce reuse, which is
essentially a way that you can extract a private key from a signature on
messages m1 and m2 using the same nonce r1.
While revealing a key might be punishment enough, we can do one better. We can
require that if a nonce is leaked, meaning some statement was equivocated, then
a some bitcoin protected by that key can be ‘stolen’ by anyone.
But this form is a little problematic, for a few reasons. Reason one is that the
oracle could cancel their bond and take it back while there are still contracts
settling with their data that they then equivocate on.
The other issue is that the funds in the punishment could be claimed by anyone,
including a miner or the oracle themselves, and especially if oracles are also miners!
To fix the first issue, we need to lock up the fund for e.g. 2 weeks and only
use the oracle for the first week to permit 1 week gap in closing. This creates a
new issue that bonds are always expiring, but maybe that’s OK.
To fix the second issue, we need a way of restricting where the funds go to definitely
be out of reach of any bad guys, e.g. burned.
CTV Fixes This.
If you had checktemplateverify, you could stipulate that a
bonded oracle must initiate a bond redemption on chain, at which point anyone
can challenge it if they know the key and they are guaranteed sufficient time
to post a challenge.
The second fix is that CTV can stipulate that the funds must be burned by
sending to an OP_RETURN, not released to miners (which would be problematic if a
miner was also an oracle).
Now our oracle is ready to sign all sorts of stuff, and we can make sure that
for a given Nonce we never sign two conflicting statements.
DLCs?
We can now use this type of oracle for a DLC protocol. We just create the contract
and then we sign+reveal using our staking key whatever messages are required. Any cheating,
and anyone who detects it can burn our money.
Attestation Chains
One of the other cools things we can do with our Bonded oracle is to sign a chain of
attestations.
For example, we could sign message 1, and then sign message 2, and then sign message 3.
We can turn this into a “blockchain” of sorts if when we sign m2 we include a
hash of m1, and when we sign m3 we include a hash of m3.
But we can go a step further. If we’re careful, we can set it up so that
‘branching’ on any message in the chain (by equivocating/producing a conflicting
statement) leaks the key of the bonded oracle with a trick I (think?) I came up
with. Here’s roughly how it works:
message 1: INIT with PK K, nonce R1 for m2, 1 BTC at risk in output X
message 2: SIGN with K, R1 H(m1), nonce R2 for m3
message 3: SIGN with K, R2 H(m1), nonce R3 for m4
If the oracle were to ever branch, it would look like this:
message 1: INIT with PK K, nonce R1, 1 BTC at risk in output X
message 2: SIGN with K, R1 H(m1), nonce R2
message 3: SIGN with K, R2 H(m2), nonce R3
message 3': SIGN with K, R2 H(m3), nonce R3'
The leak would be able to extract K’s secret key via the reuse of R2.
While it might seem that you could ‘get away with it’, because we verify at each
step that the last used nonce was from the prior step it cannot be forged. The
commitment to H(mi)
also makes it more difficult for an invalid signature to
float around since from just the top you can know what all the other states
should be.
Proof of Stake?
Essentially we’ve built a system for proof-of-stake on Bitcoin. Imagine you have
100BTC locked up in these contracts across 127 instances, and you want to run
some system based on it.
You can just download the message signed at state Mn and see what the majority
of signers voted for that slot.
Any signer who cheats gets their funds burned, and you’d learn to exclude them
from consensus.
If you do need to have a ‘rollback’, you can do it by engineering your protocol
to allow new updates to the chain of signatures to produce a rollback.
Partial Slashing
You can even implement partial slashing. Suppose you have 10 coins in a contract under
key K1. If a cheat is detected, it authorizes a txn which burns 2 and puts the
remaining 8 into key K2. The next round of slashing could put 6.4 under K3.
Alternatives to Burning
Burning sats is sad. What if instead of a burn, coins went into an annuity that
would be claimable 100 years from now? That way, no economic agents around today
can plan to cheat and capture the value of it, but the burned coins can serve a
real function. While this is slightly less secure than a full burn, it’s also
more secure since it creates an incentive to continue to build the chain.
Or donate to a well known chairty address/developer fund :p
Implementing a Staked Signer
To begin, we’ll define some ‘type tags’. This is a technique in rust where we
define empty structs that let us build a little state machine in the type
system. You can read more on the technique
here.
/// # Operational State
/// State where stakes should be recognized for voting
#[derive(JsonSchema, Deserialize)]
pub struct Operational;
/// # Closing State
/// State where stakes are closing and waiting evidence of misbehavior
#[derive(JsonSchema, Deserialize)]
struct Closing;
/// # Staking States (Operational, Closing)
/// enum trait for states
pub trait StakingState {}
impl StakingState for Operational {}
impl StakingState for Closing {}
Next, we’ll define an interface that an implementation of a Staked Signer should
implement:
By default something that is declared is given a default not-present implementation.
/// Functional Interface for Staking Contracts
pub trait StakerInterface
where
Self: Sized,
{
decl_guard!(
/// The key used to sign messages
staking_key
);
decl_guard!(
/// the clause to begin a close process
begin_redeem_key
);
decl_guard!(
/// the clause to finish a close process
finish_redeem_key
);
decl_then!(
/// The transition from Operational to Closing
begin_redeem
);
/// Why would anyone ever cheat!!
#[then(guarded_by = "[Self::staking_key]")]
fn cheated(self, ctx: sapio::Context) {
let f = ctx.funds();
ctx.template()
// commit to metadata here for convenience, but really could be anywhere!
// exercise for reader: what if we plugged in another instance of StakerInterface
// that:
// 1. switches to a new, unburned key
// 2. pays 80% to the new StakerInterface
// 3. pays 20% to an annuity that pays miners over e.g. 1000 blocks
// at some point in the far future.
.add_output(f, &Compiled::from_op_return(&self.data.as_inner()[..])?, None)?
.into()
}
}
/// We can delcare the Contract impl for all valid Staker<T>
impl<T: 'static + StakingState> Contract for Staker<T>
where
Staker<T>: StakerInterface,
T: StakingState,
{
declare! {then, Self::begin_redeem, Self::cheated}
declare! {finish, Self::finish_redeem_key}
declare! {non updatable}
}
Next, we’ll define the data required for our staker:
/// # Staker: A Bonded Signing Contract
/// Staker is a contract that proceeds from Operational -> Closing
/// During it's lifetime, many things can be signed with signing_key,
/// but should the key ever leak (e.g., via nonce reuse) the bonded
/// funds can be burned.
///
/// Burning is important v.s. miner fee because otherwise the staker
/// can bribe (or be a miner themselves) to cheat.
#[derive(JsonSchema, Deserialize)]
pub struct Staker<T: StakingState> {
/// # Timeout
/// How long to wait for evidence after closing
timeout: AnyRelTimeLock,
/// # Signing Key
/// The key that if leaked can burn funds
signing_key: PublicKey,
/// # Redemption Key
/// The key that will be used to control & return the redeemed funds
redeeming_key: PublicKey,
/// # Data
/// Arbitrary hash of metadata that is needed to start the attestation chain
data: sha256::Hash,
/// current contract state.
#[serde(skip, default)]
state: PhantomData<T>,
}
Next, we’ll define the StakerInterface when our channel is operational. At this phase,
funds can either be burnt or the redeeming key can start the process of withdrawing.
impl StakerInterface for Staker<Operational> {
/// redeeming key
#[guard]
fn begin_redeem_key(self, _ctx: Context) {
Clause::Key(self.redeeming_key)
}
/// begin redemption process
#[then(guarded_by = "[Self::begin_redeem_key]")]
fn begin_redeem(self, ctx: sapio::Context) {
let f = ctx.funds();
ctx.template()
.add_output(
f,
&Staker::<Closing> {
state: Default::default(),
timeout: self.timeout,
signing_key: self.signing_key,
redeeming_key: self.redeeming_key,
},
None,
)?
.into()
}
/// staking key
#[guard]
fn staking_key(self, _ctx: Context) {
Clause::Key(self.signing_key)
}
}
Lastly, for closing we should not be able to “loop” back into Closing or
Operational, so we do not implement the begin_redeem
logic.
impl StakerInterface for Staker<Closing> {
#[guard]
fn finish_redeem_key(self, _ctx: Context) {
Clause::And(vec![Clause::Key(self.redeeming_key), self.timeout.into()])
}
#[guard]
fn staking_key(self, _ctx: Context) {
Clause::Key(self.signing_key)
}
}
Attestation Chain
In order to start the attestation chain, the data
field should be the hash of something like:
struct AttestationStart {
/// # Nonce
/// a nonce element
first_nonce: [0u8; 32],
/// # Key
/// the key to sign with (for convenience, should match the StakedSigner's
/// staking key)
key: PublicKey,
/// # Purpose
/// useful to have some sort of description (machine readable) of what this attestor
/// is signing for
purpose: Vec<u8>
}
To start using the attestation chain, we build a linked list of Attest
signatures as described below:
enum Either<T, U> {
Left(T),
Right(U)
}
struct Attest {
/// # Signature
/// the signature over the below data fields
sig: Signature,
/// # Message
/// whatever info the protocol expects to be signed
message: Vec<u8>,
/// # Nonce
/// a nonce element
next_nonce: [0u8; 32],
/// # Height
/// what # signature is this
height: u64,
/// # Previous Attestation
/// the last attestation. we either keep a hash or the actual value
prev: Either<Hash, Either<Box<Attest>, AttestationStart>>
}
It would be possible – but perhaps overkill – to instead encode this structure
as a Sapio contract with continuation
branches. I’ll leave that as an exercise
for the reader for now!
Galaxy Brain Time
What if we used this staked signer to coordinate a decentralized mining pool
where the stakers sign off on work shares they have seen…
That’s All Folks!
Day 19: Rubin's Bitcoin Advent Calendar
16 Dec 2021
Welcome to day 19 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
For today’s post we’re going to build out some Sapio NFT protocols that are
client-side verifiable. Today we’ll focus on code, tomorrow we’ll do more
discussion and showing how they work. I was sick last night (minor burrito
oriented food poisoning suspected) and so I got behind, hence this post being up
late.
As usual, the disclaimer that as I’ve been behind… so we’re less focused today
on correctness and more focused on giving you the shape of the idea. In other
words, I’m almost positive it won’t work properly, but it can compile! And the
general flow looks correct.
There’s also a couple new concepts I want to adopt as I’ve been working on this,
so those are things that will have to happen as I refine this idea to be
production grade.
Before we start, let’s get an eagle-eye view of the ‘system’ we’re going to be
building, because it represents multiple modules and logical components.
By the end, we’ll have 5 separate things:
- An Abstract NFT Interface
- An Abstract Sellable Interface
- An Abstract Sale Interface
- A Concrete Sellable NFT (Simple NFT)
- A Concrete Sale Interface (Simple NFT Sale)

In words:
Simple NFT implements both NFT
and Sellable
, and has a sell
function that
can be called with any Sale module.
Simple NFT Sale implements Sale
, and can be used with the sell
of anything
that implements Sellable
and NFT
.
We can make other implementations of Sale
and NFT
and they should be
compatible.
How’s it going to ‘work’?
Essentially how this is going to work do is
- An artist mint an NFT.
- The artist can sell it to anyone whose bids the artist accepts
Normally, in Ethereum NFTs, you could do something for step 2:
- The artist signs “anyone can buy at this price”
with Bitcoin NFTs, it’s a little different. The artist has to run a server that
accepts bids above the owner’s current price threshold and returns signed
under-funded transaction that would pay the owner the asking price.
Alternatively, the bidder can send an open-bid that the owner can fill
immediately.
Because Sapio is super-duper cool, we can make abstract interfaces for this
stuff so that NFTs can have lots of neat features like enforcing royalties,
dutch auction prices, batch minting, generative art minting, and more. We’ll see
a bit more tomorrow.
Client validation is central to this story. A lot of the rules are not
enforced by the Bitcoin blockchain. They are, however, enforced by requiring
that the ‘auditor’ be able to re-reach the same identical contract state by
re-compiling the entire contract from the start. I.e., as long as you generate
all your state transitions through Sapio, you can verify that an NFT is
‘authentic’. Of course, anyone can ‘burn’ an NFT if they want by sending e.g.
to an unknown key. Client side validation just posits that sending to an
unknown key is ‘on the same level’ of error as corrupting an NFT by doing state
transitions without having the corresponding ‘witness’ of sapio effects to
generate the transfer.
Please re-read this section after you get throught the code (I’ll remind you).
Declaring an NFT Minting Interface
First we are going to declare the basic information for a NFT.
Every NFT should have a owner (PublicKey) and a locator (some url, IPFS hash,
etc).
NFTs also should track which Sapio module was used to mint them, to ensure
compatibility going forward. If it’s not known, modules can try to fill it in
and guess (e.g., a good gues is “this module”).
Let’s put that to code:
/// # Trait for a Mintable NFT
#[derive(Serialize, JsonSchema, Deserialize, Clone)]
pub struct Mint_NFT_Trait_Version_0_1_0 {
/// # Initial Owner
/// The key that will own this NFT
pub owner: bitcoin::PublicKey,
/// # Locator
/// A piece of information that will instruct us where the NFT can be
/// downloaded -- e.g. an IPFs Hash
pub locator: String,
/// # Minting Module
/// If a specific sub-module is to be used / known -- when in doubt, should
/// be None.
pub minting_module: Option<SapioHostAPI<Mint_NFT_Trait_Version_0_1_0>>,
}
/// Boilerplate for the Mint trait
pub mod mint_impl {
use super::*;
#[derive(Serialize, Deserialize, JsonSchema)]
pub enum Versions {
Mint_NFT_Trait_Version_0_1_0(Mint_NFT_Trait_Version_0_1_0),
}
/// we must provide an example!
impl SapioJSONTrait for Mint_NFT_Trait_Version_0_1_0 {
fn get_example_for_api_checking() -> Value {
let key = "02996fe4ed5943b281ca8cac92b2d0761f36cc735820579da355b737fb94b828fa";
let ipfs_hash = "bafkreig7r2tdlwqxzlwnd7aqhkkvzjqv53oyrkfnhksijkvmc6k57uqk6a";
serde_json::to_value(mint_impl::Versions::Mint_NFT_Trait_Version_0_1_0(
Mint_NFT_Trait_Version_0_1_0 {
owner: bitcoin::PublicKey::from_str(key).unwrap(),
locator: ipfs_hash.into(),
minting_module: None,
},
))
.unwrap()
}
}
}
Shaweeeeet! We have an NFT Minting Interface!
But you can’t actually use it to Mint yet, since we lack an Implementation.
Before we implement it…
What are NFTs Good For? Selling! (Sales Interface)
If you have an NFT, you probably will want to sell it in the future. Let’s
declare a sales interface.
To sell an NFT we need to know:
- Who currently owns it
- Who is buying it
- What they are paying for it
- Maybe some extra stuff
/// # NFT Sale Trait
/// A trait for coordinating a sale of an NFT
#[derive(Serialize, JsonSchema, Deserialize, Clone)]
pub struct NFT_Sale_Trait_Version_0_1_0 {
/// # Owner
/// The key that will own this NFT
pub sell_to: bitcoin::PublicKey,
/// # Price
/// The price in Sats
pub price: AmountU64,
/// # NFT
/// The NFT's Current Info
pub data: Mint_NFT_Trait_Version_0_1_0,
/// # Extra Information
/// Extra information required by this contract, if any.
/// Must be Optional for consumer or typechecking will fail.
/// Usually None unless you know better!
pub extra: Option<Value>,
}
/// Boilerplate for the Sale trait
pub mod sale_impl {
use super::*;
#[derive(Serialize, Deserialize, JsonSchema)]
pub enum Versions {
/// # Batching Trait API
NFT_Sale_Trait_Version_0_1_0(NFT_Sale_Trait_Version_0_1_0),
}
impl SapioJSONTrait for NFT_Sale_Trait_Version_0_1_0 {
fn get_example_for_api_checking() -> Value {
let key = "02996fe4ed5943b281ca8cac92b2d0761f36cc735820579da355b737fb94b828fa";
let ipfs_hash = "bafkreig7r2tdlwqxzlwnd7aqhkkvzjqv53oyrkfnhksijkvmc6k57uqk6a";
serde_json::to_value(sale_impl::Versions::NFT_Sale_Trait_Version_0_1_0(
NFT_Sale_Trait_Version_0_1_0 {
sell_to: bitcoin::PublicKey::from_str(key).unwrap(),
price: Amount::from_sat(0).into(),
data: Mint_NFT_Trait_Version_0_1_0 {
owner: bitcoin::PublicKey::from_str(key).unwrap(),
locator: ipfs_hash.into(),
minting_module: None,
},
extra: None,
},
))
.unwrap()
}
}
}
That’s the interface for the contract that sells the NFTs. We also need an
interface for NFTs that want to initiate a sale.
To do that, we need to know:
- What kind of sale we are doing
- The data for that sale
This is really just expressing that we need to bind a NFT Sale Implementation to
our contract. We can express the sale interface as follows.
/// # Sellable NFT Function
/// If a NFT should be sellable, it should have this trait implemented.
pub trait SellableNFT: Contract {
decl_continuation! {<web={}> sell<Sell>}
}
/// # Sell Instructions
#[derive(Serialize, Deserialize, JsonSchema)]
pub enum Sell {
/// # Hold
/// Don't transfer this NFT
Hold,
/// # MakeSale
/// Transfer this NFT
MakeSale {
/// # Which Sale Contract to use?
/// Specify a hash/name for a contract to generate the sale with.
which_sale: SapioHostAPI<NFT_Sale_Trait_Version_0_1_0>,
/// # The information needed to create the sale
sale_info: NFT_Sale_Trait_Version_0_1_0,
},
}
impl Default for Sell {
fn default() -> Sell {
Sell::Hold
}
}
impl StatefulArgumentsTrait for Sell {}
Getting Concrete: Making an NFT
Let’s create a really simple NFT now that implements these interfaces.
There’s a bit of boilerplate, so we’ll go section-by-section.
First, let’s declare the SimpleNFT
/// # SimpleNFT
/// A really simple NFT... not much too it!
#[derive(JsonSchema, Serialize, Deserialize)]
pub struct SimpleNFT {
/// The minting data, and nothing else.
data: Mint_NFT_Trait_Version_0_1_0,
}
/// # The SimpleNFT Contract
impl Contract for SimpleNFT {
// NFTs... only good for selling?
declare! {updatable<Sell>, Self::sell}
// embeds metadata
declare! {then, Self::metadata_txns}
}
First, let’s implement the logic for selling the NFT… You remember our old
friend the Sales interface?
impl SimpleNFT {
/// # signed
/// Get the current owners signature.
#[guard]
fn signed(self, ctx: Context) {
Clause::Key(self.data.owner.clone())
}
}
fn default_coerce(k: <SimpleNFT as Contract>::StatefulArguments) -> Result<Sell, CompilationError> {
Ok(k)
}
impl SellableNFT for SimpleNFT {
#[continuation(guarded_by = "[Self::signed]", web_api, coerce_args = "default_coerce")]
fn sell(self, ctx: Context, sale: Sell) {
if let Sell::MakeSale {
sale_info,
which_sale,
} = sale
{
// if we're selling...
if sale_info.data.owner != self.data.owner {
// Hmmm... metadata mismatch! the current owner does not
// matched the sale's claimed owner.
return Err(CompilationError::TerminateCompilation);
}
// create a contract from the sale API passed in
let compiled = Ok(CreateArgs {
context: ContextualArguments {
amount: ctx.funds(),
network: ctx.network,
effects: unsafe { ctx.get_effects_internal() }.as_ref().clone(),
},
arguments: sale_impl::Versions::NFT_Sale_Trait_Version_0_1_0(sale_info.clone()),
})
.map(serde_json::to_value)
// use the sale API we passed in
.map(|args| create_contract_by_key(&which_sale.key, args, Amount::from_sat(0)))
// handle errors...
.map_err(|_| CompilationError::TerminateCompilation)?
.ok_or(CompilationError::TerminateCompilation)?;
// send to this sale!
let mut builder = ctx.template();
// todo: we need to cut-through the compiled contract address, but this
// upgrade to Sapio semantics will come Soon™.
builder = builder.add_output(compiled.amount_range.max(), &compiled, None)?;
builder.into()
} else {
/// Don't do anything if we're holding!
empty()
}
}
}
Next, let’s implement the metadata logic. There are a million ways to do metadata,
so feel free to ‘skip’ this section and just let your mind wander on interesting
things you could do here…
impl SimpleNFT {
/// # unspendable
/// what? This is just a sneaky way of making a provably unspendable branch
/// (since the preimage of [0u8; 32] hash can never be found). We use that to
/// help us embed metadata inside of our contract...
#[guard]
fn unspendable(self, ctx: Context) {
Clause::Sha256(sha256::Hash::from_inner([0u8; 32]))
}
/// # Metadata TXNs
/// This metadata TXN is provably unspendable because it is guarded
/// by `Self::unspendable`. Neat!
/// Here, we simple embed a OP_RETURN.
/// But you could imagine tracking (& client side validating)
/// an entire tree of transactions based on state transitions with these
/// transactions... in a future post, we'll see more!
#[then(guarded_by = "[Self::unspendable]")]
fn metadata_txns(self, ctx: Context) {
ctx.template()
.add_output(
Amount::ZERO,
&Compiled::from_op_return(
&sha256::Hash::hash(&self.data.locator.as_bytes()).as_inner()[..],
)?,
None,
)?
// note: what if we also comitted to the hash of the wasm module
// compiling this contract?
.into()
}
}
Lastly, some icky boilerplate stuff:
#[derive(Serialize, Deserialize, JsonSchema)]
enum Versions {
Mint_NFT_Trait_Version_0_1_0(Mint_NFT_Trait_Version_0_1_0),
}
impl TryFrom<Versions> for SimpleNFT {
type Error = CompilationError;
fn try_from(v: Versions) -> Result<Self, Self::Error> {
let Versions::Mint_NFT_Trait_Version_0_1_0(mut data) = v;
let this = LookupFrom::This
.try_into()
.map_err(|_| CompilationError::TerminateCompilation)?;
match data.minting_module {
// if no module is provided, it must be this module!
None => {
data.minting_module = Some(this);
Ok(SimpleNFT { data })
}
// if a module is provided, we have no idea what to do...
// unless the module is this module itself!
Some(ref module) if module.key == this.key => Ok(SimpleNFT { data }),
_ => Err(CompilationError::TerminateCompilation),
}
}
}
REGISTER![[SimpleNFT, Versions], "logo.png"];
Right on! Now we have made a NFT Implementation. We can Mint one, but wait.
How do we sell it?
We need a NFT Sale Implementation
So let’s do it. In today’s post, we’ll implement the most boring lame ass Sale…
Tomorrow we’ll do more fun stuff, I swear.
First, let’s get our boring declarations out of the way:
/// # Simple NFT Sale
/// A Sale which simply transfers the NFT for a fixed price.
#[derive(JsonSchema, Serialize, Deserialize)]
pub struct SimpleNFTSale(NFT_Sale_Trait_Version_0_1_0);
/// # Versions Trait Wrapper
#[derive(Serialize, Deserialize, JsonSchema)]
enum Versions {
/// # Batching Trait API
NFT_Sale_Trait_Version_0_1_0(NFT_Sale_Trait_Version_0_1_0),
}
impl Contract for SimpleNFTSale {
declare! {updatable<()>, Self::transfer}
}
fn default_coerce<T>(_: T) -> Result<(), CompilationError> {
Ok(())
}
impl From<Versions> for SimpleNFTSale {
fn from(v: Versions) -> SimpleNFTSale {
let Versions::NFT_Sale_Trait_Version_0_1_0(x) = v;
SimpleNFTSale(x)
}
}
REGISTER![[SimpleNFTSale, Versions], "logo.png"];
Now, onto the logic of a sale!
impl SimpleNFTSale {
/// # signed
/// sales must be signed by the current owner
#[guard]
fn signed(self, ctx: Context) {
Clause::Key(self.0.data.owner.clone())
}
/// # transfer
/// transfer exchanges the NFT for cold hard Bitcoinz
#[continuation(guarded_by = "[Self::signed]", web_api, coerce_args = "default_coerce")]
fn transfer(self, ctx: Context, u: ()) {
let amt = ctx.funds();
// first, let's get the module that should be used to 're-mint' this NFT
// to the new owner
let key = self
.0
.data
.minting_module
.clone()
.ok_or(CompilationError::TerminateCompilation)?
.key;
// let's make a copy of the old nft metadata..
let mut mint_data = self.0.data.clone();
// and change the owner to the buyer
mint_data.owner = self.0.sell_to;
// let's now compile a new 'mint' of the NFT
let new_nft_contract = Ok(CreateArgs {
context: ContextualArguments {
amount: ctx.funds(),
network: ctx.network,
effects: unsafe { ctx.get_effects_internal() }.as_ref().clone(),
},
arguments: mint_impl::Versions::Mint_NFT_Trait_Version_0_1_0(mint_data),
})
.and_then(serde_json::to_value)
.map(|args| create_contract_by_key(&key, args, Amount::from_sat(0)))
.map_err(|_| CompilationError::TerminateCompilation)?
.ok_or(CompilationError::TerminateCompilation)?;
// Now for the magic:
// This is a transaction that creates at output 0 the new nft for the
// person, and must add another input that pays sufficiently to pay the
// prior owner an amount.
// todo: we also could use cut-through here once implemented
// todo: change seem problematic here? with a bit of work, we could handle it
// cleanly if the buyer identifys an output they are spending before requesting
// a purchase.
ctx.template()
.add_output(amt, &new_nft_contract, None)?
.add_amount(self.0.price.into())
.add_sequence()
.add_output(self.0.price.into(), &self.0.data.owner, None)?
// note: what would happen if we had another output that
// had a percentage-of-sale royalty to some creator's key?
.into()
}
}
And that’s it! Makes sense, right? I hope…
But if not
Re read the part before the code again! Maybe it will be more clear now :)
Day 18: Rubin's Bitcoin Advent Calendar
15 Dec 2021
Welcome to day 18 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 post will be a pretty different format that usual, it’s basically going
to be a pictorial walk through of the Sapio
Studio, the frontend tool for Sapio
projects. As an example, we’ll go through a Payment Pool contract to familiarize
ourselves.
I wanted to put this post here, before we get into some more applications,
because I want you to start thinking past “cool one-off concepts we can
implement” and to start thinking about reusable components we can build and ship
into a common Bitcoin Smart Contract software (Sapio Studio or its successors).
At it’s core, Sapio Studio is just a wallet frontend to Bitcoin Core.

You can make a transaction, just like normal…

And see it show up in the pending transactions…

And even mine some regtest blocks.

But where Sapio Studio is different is that there is also the ability to create
contracts.
Before we can do that, we need to load a WASM Plugin with a compiled contract.
Let’s load the Payment Pool module. You can see the code for it
here.
And now we can see we have a module!
Let’s load a few more so it doesn’t look lonely.
Now let’s check out the Payment Pool module.
Now let’s check out another one – we can see they each have different types of
arguments, auto-generated from the code.

Let’s fill out the form with 10 keys to make a Payment Pool controlled by 10
people, and then submit it.
What’s that??? It’s a small bug I am fixing :/. Not to worry…
Just click repair layout.
And the presentation resets. I’ll fix it soon, but it can be useful if there’s a
glitch to reset it.
Now we can see the basic structure of the Payment Pool, and how it splits up.
Let’s get a closer look…
Let’s zoom out (not helpful!)…
Let’s zoom back in. Note how the transactions are square boxes and the outputs
are rounded rectangles. Blue lines connect transactions to their outputs. Purple lines
connect outputs to their (potential) spends.
If we click on a transaction we can learn more about it.
We even have some actions that we can take, like sending it to the network.
Let’s try it….
Oops! We need to sign it first…
And then we can send it.
What other buttons do we have? What’s this do?
It teleports us to the output we are creating!
Notice how the output is marked “Updatable”, and there is also a “DO_TX”
button (corresponding to the DO_TX in the Payment Pool). Let’s click that…
Ooooh. It prompts us with a form to do the transaction!
Ok, let’s fill this sucker out…
Click submit, then recompile (separate actions in case we want to make multiple “moves” before recompiling).
I really need to fix this bug…
Voila!
As you can see, the original graph is intact and we’ve augmented onto it the new state transition.
The new part has our 0.1 BTC Spend + the re-creation of the Payment Pool with less funds.
Ok, let’s go nuts and do another state transition off-of the first one? This time more payouts!
Submit…
And Recompile…
I skipped showing you the bug this time.
Now you can see two state transitions! And because we used more payouts than one, we can see some congestion control at work.

It works! It all really, really works!
One more thing I can’t show you with this contract is the timing simulator.
This lets you load up a contract (like our Hanukkiah below) and…
Simulate the passing of time (or blocks).
Pretty cool!

There are also some settings you can configure for display settings, the node,
and for sapio-cli. The first time you run Sapio, you’ll need to get some of
these things configured correctly or else it will be broken. Currently, if you
look here you
can find a template for a script to get everything up and running for a first
shot at it, otherwise you’ll have to do it by hand, or just change your
preferences.json
to be similar to mine in the note.

before you ask…
OF COURSE THERE IS DARK MODE
configured by your local system theme preference

I hope you enjoyed this! There’s a metric fuckload of work still to do to make
Sapio Studio & Sapio anywhere near production grade, but I hope this has helped
elucidate how powerful and cool the Sapio approach is, and has inspired you to
build something and try it out! I’d also be really eager for feedback on what
features should be here/are missing.
Lastly, if you’re just excited about this, it’s definitely a project
that could use more experienced react/electron/bitcoin contributors, either
yourself or if you’re interested in sponsoring :).