Jeremy Rubin's Blog

Here you'll find an assorted mix of content from yours truly. I post about a lot of things, but primarily Bitcoin.

categories: Bitcoin, Shenzhen Journey.


Decentralized Autonomous Organizations (DAOs) Will Save Bitcoin

Day 25: Rubin's Bitcoin Advent Calendar

Welcome to day 25 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

The title of this article is a joke. Gotcha!

Bart Simpson writing "DAOs are just fancy multisigs".

Decentralized Autonomous Organization is pretty much what’s called an orphan initialism. So while DAO doesn’t really mean anything is decentralized, autonomous, or an organization, but the term DAO has stuck around anyways. Even moreso than NFT! More or less, DAOs are just fancy multisigs. But they’ve been used for all sorts of things, ranging from attempting to buy the US Constitution as a group, investing in startups, buying Ross Ulbricht’s NFTs, or maybe even buying my undies.

yes this is a joke

This post has some required reading. You have to have read through at least up to payment pools in the advent calendar, but ideally you’d have read all the posts…

So how will fancy-multisigs save Bitcoin? In this post we’ll work through an example of building a DAO to fund Bitcoin Core Developers, like a Bitcoin native Gitcoin competitor.


What do we want from our DAO:

The DAO will serve three functions:

  1. To Add members who pay in
  2. To make Payments if a Majority of the payers by value vote
  3. To mint commemorative NFTs

Majority rules

DAOs are little democracies, and as such we need a voting scheme to do rule changes whereby a threshold (e.g., 51%) decides what happens next. We have two options, we can either count individuals as equal, or we can weight by amount of funds contributed. We can do any threshold we like, it’s just “this many people could steal the whole pot”.

For this post, we’ll do the weighted by funds contributed because that feels closer to what’s happening in Ethereum land. Unfortunately a couple components around generated arbitrary weighted signatures1 just “aren’t quite there” or have messy tradeoffs so we won’t consider those – yet. Instead we’ll just make a silly limit: we will allow at most 24 participants2.

Implementing a DAO

First let’s define the basics. A DAO should have Members who each are ID’d by a key and have an amount of votes.

#[derive(Deserialize, JsonSchema, Serialize, Clone)]
struct Member {
    relative_votes: u64,
}

#[derive(Deserialize, JsonSchema, Clone)]
struct Dao {
    /// # Pool Members
    /// map of all initial balances as PK to BTC
    members: BTreeMap<PublicKey, Member>,
    /// The current sequence number (for authenticating state updates)
    sequence: u64,
}

impl Contract for Dao {
    declare! {updatable<Proposal>, Self::hold_vote}
}

Members can hold a vote on a proposal of some kind. Let’s do proposals that can make payments, mint NFTs, or add some noobs:

/// New Update message for generating a transaction from.
#[derive(Deserialize, JsonSchema, Serialize)]
enum Proposal {
    /// # Payments
    /// A mapping of public key in members to signed list of payouts with a fee rate.
    Payments {
        payments: BTreeMap<PublicKey, AmountU64>,
        // Some purpose for this proposal, as a String.
        reason: String,
    },
    /// # Mint
    /// Make some NFTs
    Mint {
        minting_module: SapioHostAPI<Mint_NFT_Trait_Version_0_1_0>,
        mint_data: Mint_NFT_Trait_Version_0_1_0,
    },
    /// # Add People
    Add {
        noobs: BTreeMap<PublicKey, Member>,
    },
    None,
}
/// required...
impl Default for Proposal {
    fn default() -> Self {
        Proposal::None
    }
}
impl StatefulArgumentsTrait for Proposal {}

/// helper for rust type system issue
fn default_coerce(k: <Dao as Contract>::StatefulArguments) -> Result<Proposal, CompilationError> {
    Ok(k)
}

Now we can implement the main logic of the DAO. We want it to compute keys for the majority to rule, and we want it to allow a majority to vote on a Proposal. Note how when we make a payment, unlike in the Payment Pool, we decrease all member’s proportional ownership in the pool3, so that new owners are not disadvantaged. But we could change that, to time-weight how long members have been part of the DAO as well, or give people ‘special voting weight’ disconnected from money added. It’s really up to whatever you want…

We’ll implement the logic for each type of proposal (minting, adding, or paying).

impl Dao {
    /// Sum Up all the balances
    fn total(&self) -> Amount {
        Amount::from_sat(self.members.iter().map(|e| e.1.relative_votes).sum::<u64>())
    }
    /// all signed the transaction!
    #[guard]
    fn majority_rules(self, _ctx: Context) {
        let ppl = self
            .members
            .iter()
            .map(|(m, d)| (m.clone(), d.relative_votes))
            .collect();
        // TODO: we should probably make guards return Result...
        key_groups_to_clause(
            &compute_key_groups(self.total().as_sat() / 2, ppl).expect("Well Formed"),
        )
    }
    /// This Function will create a proposed transaction that is safe to sign
    /// given a list of data from participants.
    #[continuation(
        web_api,
        guarded_by = "[Self::majority_rules]",
        coerce_args = "default_coerce"
    )]
    fn hold_vote(self, ctx: Context, update: Proposal) {
        // don't allow empty updates.
        match update {
            Proposal::None => empty(),
            Proposal::Mint {
                minting_module,
                mint_data,
            } => {
                let key = minting_module.key;
                // 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: Default::default(),
                    },
                    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)?;
                let f = ctx.funds();
                ctx.template()
                    .add_output(f, self, None)?
                    .add_output(Amount::from_sat(0), &new_nft_contract, None)?
                    .into()
            }
            Proposal::Add { mut noobs } => {
                let adding = Amount::from_sat(noobs.values().map(|m| m.relative_votes).sum());
                let mut new = self.clone();
                noobs.iter_mut().for_each(|(pk, m)| {
                    new.members
                        .entry(*pk)
                        .and_modify(|e| e.relative_votes += m.relative_votes)
                        .or_insert(m.clone());
                });
                let f = ctx.funds();
                ctx.template()
                    .add_sequence()
                    .add_amount(adding)
                    .add_output(f, self, None)?
                    .add_output(Amount::from_sat(0), &new, None)?
                    .into()
            }
            Proposal::Payments { payments } => {
                if payments.is_empty() {
                    return empty();
                }
                // collect members with updated balances here
                let spent = payments
                    .values()
                    .cloned()
                    .map(Amount::from)
                    .fold(Amount::from_sat(0), |a, b| a + b.into());
                let balance = 1.0 - (spent.as_btc() / self.total().as_btc());
                let mut new_members = self.members.clone();
                new_members.values_mut().for_each(|m| {
                    m.relative_votes = (m.relative_votes as f64 * balance).round() as u64;
                });
                // for each payment...
                // Send any leftover funds to a new pool
                let change = Dao {
                    members: new_members,
                    sequence: self.sequence + 1,
                };
                let mut tmpl = ctx.template().add_output(change.total(), &change, None)?;
                // optional: we could commit to the reason somewhere in metadata 
                // e.g. a tapleaf branch... we don't do this here because meh.
                for (key, amount) in payments {
                    tmpl = tmpl.add_output(amount.try_into()?, &key, None)?;
                }
                tmpl.into()
            }
        }
    }
}
REGISTER![Dao, "logo.png"];

Lastly, we need some super special sneaky algorithm fun to implement signing authorities based on majority value. As noted, special uses of FROST could replace this, or future research on better weighted key protocols.

For now, we limit ourselves to 25 keys so that compilation isn’t too slow. We can afford having hundreds of thousands or millions of groups because of Taproot :).

fn key_groups_to_clause<T>(v: &(Vec<(PublicKey, T)>, Vec<u32>)) -> Clause {
    Clause::Threshold(
        1,
        v.1.iter()
            .map(|m| {
                Clause::And(
                    v.0.iter()
                        .enumerate()
                        .filter_map(|(i, (k, _))| {
                            if m & (1 << i) != 0 {
                                Some(k.clone())
                            } else {
                                None
                            }
                        })
                        .map(Clause::Key)
                        .collect(),
                )
            })
            .collect(),
    )
}
fn compute_key_groups(
    threshold: u64,
    mut el: Vec<(PublicKey, u64)>,
) -> Result<(Vec<(PublicKey, u64)>, Vec<u32>), CompilationError> {
    if el.len() > 25 || el.is_empty() {
        return Err(CompilationError::TerminateCompilation);
    }
    // sort for stable ordering
    el.sort();
    // The bitmasks for which keys to participate
    let mut sets: Vec<u32> = vec![];
    // BEGIN ALGORITHM:
    // if we see a bit set out of range, we can stop.
    let fail_if_set = ((!0) >> el.len()) << el.len();
    // we know that 0 elements is invalid, we need up to el.len()
    for i in 1u32..=el.len() as u32 {
        // get the first member of our permutation
        let mut ct = element_0(i);
        // if any bits are set in the failure zone stop
        while ct & fail_if_set == 0 {
            // compute the sum of the elements in this mask
            let sum: u64 = (0..el.len())
                .map(|i| if ct & (1 << i) != 0 { el[i].1 } else { 0 })
                .sum::<u64>();
            // this set is a candidate!
            if sum >= threshold {
                // subtract the smallest value (this is why we sorted) -- if it
                // fails it is not a minimal set because there exists a passing
                // set without this element.
                // note: trailing zeros is guaranteed to be in bounds
                if sum - el[ct.trailing_zeros() as usize].1 < threshold {
                    // it did fail, so save it
                    sets.push(ct);
                }
            }
            // get the next ct
            ct = next_perm(ct);
        }
    }
    Ok((el, sets))
}

/// Adapted from https://www.alexbowe.com/popcount-permutations/
///
/// Compute the lexicographically next bit permutation
/// Taken from http://graphics.stanford.edu/~seander/bithacks.html
fn next_perm(v: u32) -> u32 {
    let t: u32 = v | (v - 1); // t gets v's least significant 0 bits set to 1
                              // Next set to 1 the most significant bit to change,
                              // set to 0 the least significant ones, and add the necessary 1 bits.
    let w: u32 = (t + 1) | (((!t & (!t).wrapping_neg()) - 1) >> (v.trailing_zeros() + 1));
    w
}
///    Generates first permutation with a given amount of set bits, which is
///    used to generate the rest.
fn element_0(c: u32) -> u32 {
    return (1 << c) - 1;
}

All done! Not too bad huh? I think you’re really getting the hang of this thing!


Using a DAO

Now that we have this DAO we can get together a group of people and share a UTXO.

With that shared balance, we can get everyone in some kind of chat room and ‘govern’ what proposals folks want to vote on.

In particular, I would be very excited to see DAOs emerge for funding Bitcoin Developers. This type of structure can potentially help folks communally allocate capital. Often times the biggest barrier is finding deals that make sense, and DAOs would enable you to share with a group of friends and they could make decisions for you.

It would even be possible to create DAOs on behalf of third parties and fund them. For example, let’s say I get PKs for 10 devs I like and put a 10 BTC into it and set the shares up so that there is a ‘leader’ with 30%, and the rest split 70% of voting shares. The leader could just steal the money with another 21%, but would they? I hope not! Instead, they can vote on good things as intended. It’d also be possible for the DAO creator to embed an ‘oversight comittee’ that can yank the funds if not being used.

Minting NFTs is kind of a cool feature since anyone can see they came from the DAO if they track the DAO’s state updates (conceivably these get published for auditing). NFTs could be issued as medals of honor for devs who follow their grants. Or, if you really like NFTs, they could be used to issue software licenses in exchange for contributing funds to the DAO operators.

Does this need CTV?

Nope. Just a fancy multisig, right?

Where CTV is useful is if we want to vote on proposals to put things into CTV contracts, like subscriptions to developer grants, opening channels, etc. Imagine the developer gets a contract where they get paid out every week, but there is a auditing comittee that can be used to terminate the subscription and return funds to the DAO if misbehavior is detected.

While you don’t need CTV in the DAO backbone, it would help open up new use cases.

It would also be possible to add some ‘liveness’ smooth degradations of the DAO, whereby half the majority (e.g., if majority is 50%, 25%) could vote that the DAO is dead, and after a period of time for the majority recovery, distribute the funds on a pre-comitted schedule.

We don’t show that here, but it wouldn’t be too hard now would it?

Generalizing

One could go ahead and implement a DAO trait that all DAOs could share and build a common UX for managing DAOs with a wide variety of custom logic…

It’d also be possible to have a DAO backbone which is a single UTXO, and have other UTXOs ‘owned’ by the DAO that can get merged in later as a proposal. This way contributions to the DAO don’t always require a state update from the DAO itself.

For future work :)

  1. FROST allows n-M threshold Schnorr signatures, which can turn into a weighted solution by making M the total value and each party have W amount of keys for their contribution. But this scales poorly because you need to exchange keys and signatures linear in the Amount, which is up to a 51-bit number. 

  2. We are going to brute force all the N-N key combinations, so we pick a low number like 24 and things stay ‘small’ enough. 

  3. please please if you’re implementing this for real use rational types not floats. 



POWSWAP: Oracle Free Bitcoin Hashrate Derivatives

Day 24: Rubin's Bitcoin Advent Calendar

Welcome to day 24 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 near and dear to my heart – years ago I put up an interest form for powswap.com, but as I went down the rabbit hole I realized how badly I wanted generic tooling to automate the building of these which is partly what led to Sapio!

So therefore it’s very exciting to show you the basics of powswap in Sapio. You can see how bad the early version was here. If you want to contrast life with Sapio and without.

What is a Powswap?

The basic idea of Powswap is super simple. It is a contract that measures a block surplus or deficit – a Block Delta Contract (BDC). A BDC allows counterparties to bet on statements like “at the end of 6 months, we will be +/- 1000 blocks against the current expected number of blocks”, and program a payoff curve based on the binary outcome of that. The block delta should be – and this is a matter for the analysts to price on and model – correlated with changes in hashrate.

WHO THE F CARES

Well imagine you are about to buy a new fancy mining rig to mine with. But you have a moment of doubt – what if everyone else is doing that right now too?

You could buy hashrate derivatives where you win money if the hashrate increases and lose if it stays the same.

This would de-risk your investment in mining.

You can also lever-up and increase profit if you’re adding a lot of hashrate, doubling down that hashrate goes up, but let’s not entertain the degens shall we.

OK OK How can I do it?

One could imagine making a BDC based on the Oracle system we saw in yesterday’s post. But the magic of Powswap is that we will do this without using any oracle whatsoever, just measuring the blocks directly.

How do we do this?

The answer is actually really simple. Suppose Alice wants to get 1 Bitcoin if 100 blocks are missing at the end of the week (the 28th, let’s say expected 1000 blocks), and Bob wants to win 1 Bitcoin if they are actually there.

All we have to do is have Alice and Bob agree to a multisig to deposit 0.5 BTC each to, and then pre-sign from it two transactions:

  1. If the date is the 28th at noon and the height is greater than 1000, Bob gets paid 1 BTC
  2. If the date the date is the 28th at noon + 8 hours, Alice gets paid 1 BTC

Let’s think it through:

Suppose that Alice is right and blocks are 100 short by noon.

In the next 8 hours, only 48 blocks should be mined (and probably less, if the hashrate has actually decreased).

After that point, Alice has 8 more hours (again, probably more if hashrate actually decreased) to broadcast and claim her BTC.

Suppose that Bob is right and blocks reach 1000 at noon. Bob has 8 hours to claim the BTC before Alice can.

Metastability

Where this is a bit wonky is that the result is metastable. Let’s assume that neither Alice nor Bob is right: The deficit is 50 blocks short.

At noon, Bob cannot claim. But in 8 hours he can! But also in 8 hours Alice can claim too.

So who wins?

The answer is either! Using a POWSWAP you either want to be really right or really wrong.

We’ll see some cool results around why this might not be a huge deal later.


Let’s flip the powswap around now, for a surplus of blocks. Bob thinks the blocks will be 1000, Alice thinks 1100.

  1. If the date is the 28th at noon + 8 hours, Bob gets paid 1 BTC
  2. If the date the date is the 28th at noon and there are 1100 blocks, Alice gets paid 1 BTC

Under this model if Alice is right there should be that many blocks by that time, and if Bob is right there should not be and a resaonable amount of time later Bob can claim.


It’s a bit harder to see, but we can even implement this logic more simply as just:

  1. If we reach +1 week, give Bob 1 BTC
  2. If we reach +1100 blocks, give Alice 1 BTC

Then, if a week goes by first without seeing 1100 blocks, Bob can claim. If the 1100 blocks show well before the week is up, then Alice can claim. If neither are really right then it’s metastable and either could win.

We are not going there!

There are a myriad of different combinations of locktimes and heights that you can use to do this correctly, we won’t focus too much on that in this post, and we’ll let our contract users decide what parameters they want. Let the analysts figure out what the right combo of locktimes and stuff is to hedge different risks. They should get paid for something, right?

One of the wrinkles is that the less time you have in your contract, the more metastable it is. The more time you have, expecially across difficulty adjustments, the more the deficits can be erased.

Implementing a Powswap

Is CTV Required?

In the example I gave above, it is not! However, if you have CTV then one party can unilaterally open a hashrate derivative for other parties, and that matters quite a lot!

This means that when we do implement it, we will use then because if you want the pre-signature version you can use CTV Emulators.


First we’ll start by writing some code to be able to describe the locktimes under which some outcome is considered “resolved”. We’ll write a container type (the data we actually need) and then we’ll write a verifier type that makes for a convenient API for human input. It’s kind of gross, so you can skip the verifier type code and just imagine you put in the correct parameters.

/// `ContractVariant` ensures that we either set a Relative Height and Absolute
/// Time or a Relative Time and Absolute Height, the two valid combinations, or
/// just one.
///
/// Note these are unlocking conditions for each participant.
///
/// Validity is ensured through smart constructor
#[derive(JsonSchema, Deserialize, Clone, Copy)]
#[serde(try_from = "ValidContractVariant")]
pub struct ContractVariant(Option<AnyRelTimeLock>, Option<AnyAbsTimeLock>);

/// In order to test for coherence here, we should convert
/// ValidContractVariant to ContractVariant.
///
/// The coherence rules should match one ruleset of:
/// - a single type of TimeLock (Relative Height, Relative Time, Absolute Time,
///   Absolute Height)
/// - a mixed TimeLock of just Relative Height/Absolute Time or just Relative
///   Time/Absolute Height
#[derive(JsonSchema, Deserialize, Clone)]
struct ValidContractVariant(Vec<AnyTimeLock>);

impl TryFrom<ValidContractVariant> for ContractVariant {
    type Error = CompilationError;
    fn try_from(vcv: ValidContractVariant) -> Result<Self, Self::Error> {
        let abs: Vec<_> = vcv
            .0
            .iter()
            .filter_map(|v| {
                if let AnyTimeLock::A(a) = v {
                    Some(a)
                } else {
                    None
                }
            })
            .collect();
        let rel: Vec<_> = vcv
            .0
            .iter()
            .filter_map(|v| {
                if let AnyTimeLock::R(r) = v {
                    Some(r)
                } else {
                    None
                }
            })
            .collect();

        let all_rh = rel.iter().all(|v| matches!(v, AnyRelTimeLock::RH(c)));
        let all_rt = rel.iter().all(|v| matches!(v, AnyRelTimeLock::RT(c)));
        #[derive(Debug)]
        struct LocalError(&'static str);
        impl std::fmt::Display for LocalError {
            fn fmt(
                &self,
                f: &mut std::fmt::Formatter<'_>,
            ) -> std::result::Result<(), std::fmt::Error> {
                self.0.fmt(f)
            }
        }
        impl std::error::Error for LocalError {}
        if !(all_rh || all_rt) {
            return Err(CompilationError::custom(LocalError(
                "Must have some timelock set!",
            )));
        }
        let all_ah = abs.iter().all(|v| matches!(v, AnyAbsTimeLock::AH(c)));
        let all_at = abs.iter().all(|v| matches!(v, AnyAbsTimeLock::AT(c)));
        if !(all_ah || all_at) {
            return Err(CompilationError::custom(LocalError(
                "Incoherent Absolute Timelocks (mixed height/time)",
            )));
        }

        let relative = rel.iter().max_by_key(|v| AnyRelTimeLock::get(v)).cloned();
        let absolute = abs.iter().max_by_key(|v| AnyAbsTimeLock::get(v)).cloned();

        if matches!((relative, absolute), (None, None)) {
            return Err(CompilationError::custom(LocalError(
                "Must have some timelock set!",
            )));
        }

        if (all_rt && all_at) || (all_rh && all_rt) {
            return Err(CompilationError::custom(LocalError(
                "Must mix {Relative,Absolute} Height and Absolute time!",
            )));
        }
        Ok(ContractVariant(relative.cloned(), absolute.cloned()))
    }
}
impl ContractVariant {
    fn get_relative(&self) -> AnyRelTimeLock {
        self.0.unwrap_or(RelTime::from(0).into())
    }
    fn get_abs(&self) -> AnyAbsTimeLock {
        self.1.unwrap_or(AbsHeight::try_from(0).unwrap().into())
    }
}

With that out of the way, let’s now define our contract data:

/// Instructions for a Payment from an outcome
#[derive(JsonSchema, Deserialize, Clone)]
pub struct Pays {
    sats: AmountU64,
    to: PublicKey,
}
/// A `Outcome` is a contract where
#[derive(JsonSchema, Deserialize, Clone)]
pub struct Outcome {
    /// # Variant
    /// if the base is time or height for the relative leg.
    unlocks_if: ContractVariant,
    /// # Outcome
    /// Payments to make (should be >= 1)
    outcome: Vec<Pays>,
}
/// A `PowSwap` is a contract where
#[derive(JsonSchema, Deserialize, Clone)]
pub struct PowSwap {
    /// # Parties
    pub outcomes: [Outcome; 2],
    /// # Cooperate Key
    coop: Vec<PublicKey>,
}
impl Contract for PowSwap {
    declare! {then, Self::payoff}
    declare! {finish, Self::cooperate}
}

As you can see, it’s pretty simple. We just need a set of keys to ‘opt out’ of the on-chain execution, and a set of outcomes and their unlocking conditions. We can pay an arbitrary number of parties.

Now to finish, let’s implement the logic. It’s really simple, we just create the (2) transactions and assign the sequences/locktimes properly.

impl PowSwap {
    #[guard]
    fn cooperate(self, ctx: Context) {
        Clause::And(self.coop.iter().cloned().map(Clause::Key).collect())
    }
    fn make_payoffs(&self, ctx: Context, payments: &[Pays]) -> Result<Builder, CompilationError> {
        let mut bld = ctx.template();
        for Pays { sats, to } in payments {
            bld = bld.add_output(sats.clone().into(), to, None)?;
        }
        Ok(bld)
    }
    #[then]
    fn payoff(self, mut base_ctx: Context) {
        let mut ret: Vec<Result<Template, _>> = vec![];
        for (i, path) in self.outcomes.iter().enumerate() {
            let ctx = base_ctx.derive_num(i as u64)?;
            let v = self
                .make_payoffs(ctx, &path.outcome)?
                .set_sequence(-1, path.unlocks_if.get_relative())?
                .set_lock_time(path.unlocks_if.get_abs())?
                .into();
            ret.push(Ok(v));
        }
        Ok(Box::new(ret.into_iter()))
    }
}

That wasn’t so bad now, was it?

Using PowSwap

We already said we’re not going to analyze the profit of these contracts, but I want to give a couple cool ways to use these.

When to Cooperate?

One thing that I think would be important to settling a hashrate derivative would be to set it for, say, 6 months forecast and then try to roll the strategy at 3 months cooperatively.

This way you don’t have trouble with metastability as you and your counterparty can update forecasts and re-enter the contract, or go separate ways.

TAKE IT TO THE LIMIT

Well what if instead of settling on-chain, you nested these in LN channels? And then every microsecond you don’t see a block being advertised and broadcast, you update your probabilities and try to adjust with your counterparty. It becomes pretty neat becuase you essentially make a hashrate perpetual where if your counterparty dies then you settle on-chain (if they’re really dead, you just win), but you can update your forecasts on whatever frequency you want. All trustlessly.

The Information Market for Relaying

This opens the door for HFT-ing information about the rate of block production. Knowing a block is mined and getting it relayed to you before your counterparty gives you an edge in trading.

Maybe this pays for really really good block relaying infrastructure?

GALAXY BRAIN ME

Hey, it’s me. Your old friend Decentralized Coordination Free Mining Pools. What if we made – using CTV – the channels/payouts by default resolve into some sort of hashrate future, and we had an automated hedging market maker that could incorporate your desired side of a trade from old hash shares into opening new positions for you every block. If it was in channels you could immediately turn these into hashrate perps.

Metastability

If you’re a miner and you mine, say, 2 blocks a day, then you can usually expect to be able to settle your own metastable hashrate derivatives as long as the metastable window isn’t smaller than ~12 hours. This means that while normie pleb users might struggle with closing their derivatives, miner-to-miner hashrate derivatives should be actually pretty safe if you stay in your bounds.


Can we really do this? How effective is it? Honestly I have no idea I just think it’s mind-blowing.

Gimme all My Options

This idea composes beautifully with the options we saw yesterday. What if I want the option for the next week to open up a 6 month hashrate contract with you?

Just toss it into an Expiring Under Funded Option contract and you got it. And because we represented these as Dutch Auctionable NFTs, you can advertise the position you’re willing to open to the network and take the best offer for this option.

Sapio composes. Legit forreal.

DeFi is coming to Bitcoin.

And it’s going to help with securing the base layer of Bitcoin by permitting trustless financialization of investments in hashrate.


Have a great day. P.s. now is a good time to join utxos.org/signals if you think CTV is a great next step in Bitcoin Development’s journey.



Derivatives and Options For Bitcoin

Day 23: Rubin's Bitcoin Advent Calendar

Welcome to day 23 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

In today’s post we’re going to talk about derivatives and options. Hoooo Boy!

Let’s define an Option:

An option is a contract that gives the holder the right to take an action to the detriment of a counterparty. Options can be created for payment.

For example, I might say to you, “hey! I heard you’re pretty good at coming up with memes. I’d like to pay you $10 for the option to buy the next meme you make supporting OP_CTV for $100.”1 You might say “OK”, and then make a meme. I think it’s awful and I tell you to go away. I’m out the $10, but not $110! I can’t post the meme though or my friends will think I’m lame for sharing Right Clicked Content. Or, if I think it’s great, I can pay you the $100 and then I’m off to the races.

There are a few different types of Option contract to think about:

Call v.s. Put.

A Call option we get the right to buy something later for a fixed price (like a preorder).

A Put option we get the right to sell something later for a fixed price (like insurance).

Here’s how to remember it:

Think about a nice puppy. You Call the puppy to you, and give him a cookie, head pat, and a “good boy”.

Think about a naughty puppy. You take away your chewed up sneaker, and Put her in her crate. “Bad girl”.

American v. European

American options you can settle at any time before it “expires”.

European options you must settle during a specific window after it expires, and before the window expires.

Think of European options like a restaurant reservation. You can no-show if you want, but you can show up between 7:00 and 7:15 and be seated.

American options are like a hold on jacket you think is beautiful at Saks Fifth Ave. You could come and buy it later today, tomorrow even! But wait a week and someone else will buy it because they put it back on the rack.

Collateralized v. Non Collateralized

A collateralized option means that the asset is actually there.

For example, think of Jerry Seinfeld renting a car. He reserved the car (buying a call option), but they didn’t have a car when he showed up. They knew how to take the reservation, but not keep it.

This is an example of a ‘naked short sale’ of the car rental, because it wasn’t backed by an actual car to rent.

I don’t have a proof, but we can’t really build that kind of thing in Bitcoin.

However, imagine if Jery had to, in order to make his reservation, make a deposit for the entire value of the reservation. Sure, he could get a refund, but then he would have an opportunity cost of which options. Imagine you’re taking your partner out for a suprise dinner that’s going to cost $1000, but you don’t know which one is going to be better so you reserve two restaurants and cancel one you decide not to go to for a 1% penalty. If you had to deposit, it would cost you $10 for the no-show, but you’d have to put up $2000 to hold the reservations! If instead, you paid both restaurants $10 up front, then you would only need to lock up $20 instead. Much more efficient!

These, we will build.

The Optimal Strategy for Pricing Options

Just kidding. I have no idea. It’s a complex subject, but some people are OK at it. This post is just plumbing.

What’s Derivative?

Your humor…. burnnnn. Just kidding.

A derivative is uhhh… well. It’s anything that isn’t the thing?

A derivative is a way of taking a real thing (e.g. a ton of corn, an NFT, an Apple Stock) and then either wrapping it or observing it in some other financial product.

In fact, Options themselves are Derivatives! Wacky, right? It’s a thing (e.g. a car) and then the right to buy that car has a price and a value that is a function of what the car is, but a lot of other factors too. So the option isn’t the car, but it’s connected.

An option is a “Real Derivative” because it is actually connected to the car that is bought or sold. But we can also make “Synthetic Derivatives” that just measure some external quantity (somehow) and then give you some amount of value in return. For example, I could make a synthetic option that mints an NFT of a car instead of the actual car. Or I could make a synthetic derivative that measures the price of the car over the last month and gives me that value in Bitcoin at the end of the month.

For synthetics, they have to be over collateralized to cover all outcomes. E.g., if we expect the car to be $50,000, but it might go to $100,000, we have to lock up $100,000. And if the price is $200,000, well our max profit is $100k then.

The Options we saw earlier were very binary in outcome. Synthetic derivatives like these can emulate any function, discontinuous or continuous. So you could have a contract, for example, that pays out on a sinusoidal wave based on the car price. Trippy.

Wheres the info come from?

Well, multiple places. We could get it from a third party (maybe using an attestation chain of some sort?), or there are certain ways it could be self-referential (like for powswap).

Let’s See Some Code

Synthetic Derivatives

I love the children equally, so let’s start with Derivatives now.

First let’s define an Oracle Interface who provides us data. All the Oracle does is, given a Symbol (some request), gives us a Clause they will help us satisfy if the Symbol is true, and something else if it is false. Imagine a symbol that you can query an oracle for questions such as “is a Bitcoin worth more than $50k”.

/// Placeholder type for a standard way of looking up a stock symbol; can be defined more
/// concretely but should have a human readable string representation.
pub type Symbol = String;
/// Oracle is a generic wrapper for any logic to get a pair of binary clauses.
/// It can be based on hash preimage, federated signers, or key revealing.
/// The Trait Object can be responsible for network requests/caching.
pub trait Oracle {
    /// returns keys (price lo, price hi) for the given query
    fn get_key_lt_gte(&self, t: &Symbol, price: i64) -> (Clause, Clause);
}

Now let’s define a threshold oracle – we wouldn’t want to trust just one lousy oracle, so let’s trust M out of N of them!

/// An Oracle can also be "composed" into a threshold scheme with other
/// oracles quite easily as below...
///
/// Under *certain* circumstances, composition could be optimized (e.g., schnorr keys)
pub struct ThresholdOracle {
    /// the list of price oracles to consult
    pub oracles: Vec<Box<dyn Oracle>>,
    /// how many oracles must agree
    pub thresh: usize,
}

impl Oracle for ThresholdOracle {
    fn get_key_lt_gte(&self, t: &Symbol, price: i64) -> (Clause, Clause) {
        let (l, r) = self
            .oracles
            .iter()
            .map(|o| o.get_key_lt_gte(t, price))
            .unzip();
        (
            Clause::Threshold(self.thresh, l),
            Clause::Threshold(self.thresh, r),
        )
    }
}

The underlying clauses could really be anything… we can even (with some tweaks to Sapio I’d LOVE to get working, but need to engineer) make this represent Discrete Log Oracles with 2 counterparties and an external Oracle. If that means something to you, good, otherwise you can ignore that remark.

Now, let’s define a Generic framework for any outcome. The key insight we need to have is that we can ask the oracle a bunch of greater-than-or-less-than questions and build up a binary tree of transactions to settle at the right price.

To start, let’s define some basic stuff for a ‘GenericBet’.

/// A GenericBet takes a sorted list of outcomes and a cached table of
/// oracle lookups and assembles a binary contract tree for the GenericBet
pub struct GenericBet {
    amount: Amount,
    outcomes: Vec<(i64, Template)>,
    oracle: Rc<HashMap<i64, (Clause, Clause)>>,
    cooperate: Clause,
}
impl Contract for GenericBet {
    declare!(then, Self::pay_gte, Self::pay_lt, Self::oracle_no_show);
}

But where do we get the list of price to outcome and price to oracle clause from?

We need an external data source, right?

We’ll define some arguments and then a way of turning those arguments into a precise GenericBet.

We do it this way so that we can have GenericBetArguments accept a non-deterministic oracle server type, and then GenericBet itself could live in WASM and be fully deterministic.

/// To setup a GenericBet select an amount, a list of outcomes, and an oracle.
/// The outcomes do not need to be sorted but must be unique.
pub struct GenericBetArguments<'a> {
    amount: Amount,
    outcomes: Vec<(i64, Template)>,
    oracle: &'a dyn Oracle,
    cooperate: Clause,
    symbol: Symbol,
}
/// We can then convert the arguments into a specific contract instance
impl<'a> From<GenericBetArguments<'a>> for GenericBet {
    fn from(mut v: GenericBetArguments<'a>) -> GenericBet {
        // Make sure the outcomes are sorted for the binary tree
        v.outcomes.sort_by_key(|(i, _)| *i);
        // Cache locally all calls to the oracle
        let mut h = HashMap::new();
        for (k, _) in v.outcomes.iter() {
            let r = v.oracle.get_key_lt_gte(&v.symbol, *k);
            h.insert(*k, r);
        }
        GenericBet {
            amount: v.amount,
            outcomes: v.outcomes,
            oracle: Rc::new(h),
            cooperate: v.cooperate,
        }
    }
}

Now, we’ll implement the logic behind a generic bet:

Basically, we do a binary search over all the outcomes to find the middle, and if the price is greater, we send to that one. Otherwise, the other one. By winnowing through all of these outcomes recrusively, we are able to resolve a single price:action pair and settle the contract.

impl GenericBet {
    /// The oracle price kyes for this part of the tree is in the middle of the range.
    fn price(&self, b: bool) -> Clause {
        let v = &self.oracle[&self.outcomes[self.outcomes.len() / 2].0];
        if b {
            v.1.clone()
        } else {
            v.0.clone()
        }
    }
    fn recurse_over(
        &self,
        range: std::ops::Range<usize>,
        ctx: sapio::contract::Context,
    ) -> Result<Option<Template>, CompilationError> {
        match &self.outcomes[range] {
            [] => return Ok(None),
            [(_, a)] => Ok(Some(a.clone())),
            sl => Ok(Some(
                ctx.template()
                    .add_output(
                        self.amount.into(),
                        &GenericBet {
                            amount: self.amount,
                            outcomes: sl.into(),
                            oracle: self.oracle.clone(),
                            cooperate: self.cooperate.clone(),
                        },
                        None,
                    )?
                    .into(),
            )),
        }
    }
    /// Action when the price is greater than or equal to the price in the middle
    #[guard]
    fn gte(self, _ctx: Context) {
        self.price(true)
    }
    #[then(guarded_by = "[Self::gte]")]
    fn pay_gte(self, ctx: sapio::Context) {
        if let Some(tmpl) = self.recurse_over(self.outcomes.len() / 2..self.outcomes.len(), ctx)? {
            Ok(Box::new(std::iter::once(Ok(tmpl))))
        } else {
            Ok(Box::new(std::iter::empty()))
        }
    }

    /// Action when the price is less than or equal to the price in the middle
    #[guard]
    fn lt(self, _ctx: Context) {
        self.price(false)
    }
    #[then(guarded_by = "[Self::lt]")]
    fn pay_lt(self, ctx: sapio::Context) {
        if let Some(tmpl) = self.recurse_over(0..self.outcomes.len() / 2, ctx)? {
            Ok(Box::new(std::iter::once(Ok(tmpl))))
        } else {
            Ok(Box::new(std::iter::empty()))
        }
    }
    /// Allow for both parties to cooperative close
    #[guard]
    fn cooperate(self, _ctx: Context) {
        self.cooperate.clone()
    }

    #[then]
    fn oracle_no_show(self, _ctx: Context) {
        // elided for simplicity: unilateral close initiation after certain
        // relative delay if oracle doesn't reveal data
    }
}

This is, by itself, useless. But now that we have it we can implement now any payoff curve we want to. I’ll just show one example and leave it as “homework” for you to build others. We’ll start with the humble risk-reversal, which can be used to stablize a Bitcoin against the dollar. You can think of it as a Bitcoin “low pass” filter: you’ll still see big price swings, but not little ones. See below:

  Value of BTC in Asset
     |            
     |                                 /
     |             a                  /
     |        <------         b      /
     |               -------------> /
     |        ----------------------
     |       /       ^
     |      /        |
     |     /        current price
     |    /
     --------------------------------------------------- price of BTC in Asset
  Amount of BTC
     |            
     |-------
     |       \
     |        \  ^
     |         \  \
     |          \  \
     |           \  \
     |            \  \  a
     |             \  \
     |              \  \
     |               \  \
     |                \  \
     |                 \ <- current price
     |                  \  \
     |                   \  \
     |                    \  \
     |                     \  \ b
     |                      \  \
     |                       \  \
     |                        \  \
     |                         \  \
     |                          \  \
     |                           \  \
     |                            \  \
     |                             \  v
     |                              \
     |                               --------------
     |    
     --------------------------------------------------- price of BTC in Asset

In this case, Operator would be providing enough Bitcoin (Y) for a user’s funds (X) such that:

\((current - a)*(X+Y) = current * X\) or \(Y * current = a * (X + Y)\)

and would be seeing a potential bitcoin gain (Z) of

\((current + b) * (X - Z) = current * X\) or \(Z = b * X / (b + current)\)

or \(Z (current + b)\) dollars.

Operator can profit on the contract by:

  1. selecting carefully parameters a and b
  2. charging a premium
  3. charging a fee (& rehypothecating the position)

Similar to our GenericBetArguments we’ll compile this to a GenericBet to hide all the Network-y stuff.

First, let us define a couple APIs we need for the maker and taker of a contract (e.g., the person offering dollar stabilization and the person needing it).

/// An API for the Operator Service:
pub trait OperatorApi {
    /// Return Operator's Oracle
    fn get_oracle(&self) -> &dyn Oracle;
    /// Get a fresh key clause for Operator signing (could be a multisig etc)
    fn get_key(&self) -> Clause;
    /// Get a contract for a receivable amount. Allows Operator to direct funds to e.g.
    /// cold storage contracts
    fn receive_payment(&self, amount: Amount) -> Compiled;
}

/// An API for the Counterparty
pub trait UserApi {
    /// Get a fresh key clause for user signing (could be a multisig etc)
    fn get_key(&self) -> Clause;
    /// Get a contract for a receivable amount. Allows Userto direct funds to e.g.
    /// cold storage contracts
    fn receive_payment(&self, amount: Amount) -> Compiled;
}

Now, let us define the Arguments to a Risk Reversal:

//! RiskReversal represents a specific contract where we specify a set of price ranges that we
//! want to keep purchasing power flat within.
pub struct RiskReversal<'a> {
    amount: Amount,
    /// the current price in dollars with one_unit precision
    current_price_x_one_unit: u64,
    /// price multipliers rationals (lo, hi) and (a,b) = a/b
    /// e.g. ((7, 91), (1, 10)) computes from price - price*7/91 to price + price*1/10
    range: ((u64, u64), (u64, u64)),
    // ignore the
    operator_api: &'a dyn apis::OperatorApi,
    user_api: &'a dyn apis::UserApi,
    symbol: Symbol,
    ctx: Context,
}

Lastly, a bunch of complicated logic to turn those arguments into a price curve for a GenericBetArguments that then gets turned into a GenericBet:

const ONE_UNIT: u64 = 10_000;
impl<'a> TryFrom<RiskReversal<'a>> for GenericBetArguments<'a> {
    type Error = CompilationError;
    fn try_from(mut v: RiskReversal<'a>) -> Result<Self, Self::Error> {
        let key = v.operator_api.get_key();
        let user = v.user_api.get_key();
        let mut outcomes = vec![];
        let current_price = v.current_price_x_one_unit;
        // TODO: Can Customize this logic to for arbitrary curves or grids
        // bottom and top are floor/ceil for where our contract operates
        let bottom =
            ((current_price - (current_price * v.range.0 .0) / v.range.0 .1) / ONE_UNIT) * ONE_UNIT;
        let top = (((current_price + (current_price * v.range.1 .0) / v.range.1 .1) + ONE_UNIT
            - 1)
            / ONE_UNIT)
            * ONE_UNIT;
        // The max amount of BTC the contract needs to meet obligations
        let max_amount_bitcoin = (v.amount * current_price) / bottom;

        // represents an overflow
        if bottom > current_price || top < current_price {
            return Err(CompilationError::TerminateCompilation);
        }

        let mut strike_ctx = v.ctx.derive_str(Arc::new("strike".into()))?;
        // Increment 1 dollar per step
        for strike in (bottom..=top).step_by(ONE_UNIT as usize) {
            // Value Conservation Property:
            // strike * (amount + delta)  == amount * current price
            // strike * (pay to user)  == amount * current price
            // pay to user  == amount * current price / strike
            let profit = (v.amount * current_price) / strike;
            let refund = max_amount_bitcoin - profit;

            outcomes.push((
                strike as i64,
                strike_ctx
                    .derive_num(strike as u64)?
                    .template()
                    .add_output(profit, &v.user_api.receive_payment(profit), None)?
                    .add_output(refund, &v.operator_api.receive_payment(refund), None)?
                    .into(),
            ));
        }
        // Now that the schedule is constructed, build a contract
        Ok(GenericBetArguments {
            // must send max amount for the contract to be valid!
            amount: max_amount_bitcoin,
            outcomes,
            oracle: v.operator_api.get_oracle(),
            cooperate: Clause::And(vec![key, user]),
            symbol: v.symbol,
        })
    }
}

impl<'a> TryFrom<RiskReversal<'a>> for GenericBet {
    type Error = CompilationError;
    fn try_from(v: RiskReversal<'a>) -> Result<Self, Self::Error> {
        Ok(GenericBetArguments::try_from(v)?.into())
    }
}

Woooooop! Our Risk has been Reversed!

Options

First let’s play with Options.

We need a generic trait interface for all options. The basics are something to happen when it expires, and something to happen when it is paid for (strikes).

/// Generic functionality required for Expiring contracts
pub trait Expires: 'static + Sized {
    decl_then! {
        /// What to do when the timeout expires
        expires
    }
    decl_then! {
        /// what to do when the holder wishes to strike
        strikes
    }
}

First we’ll define an ExpiringOption whereby two parties deposit all the funds required for the contract (full collateral).


/// Wraps a generic option opt with functionality to refund both parties on timeout.
pub struct ExpiringOption<T: 'static> {
    party_one: Amount,
    party_two: Amount,
    key_p1: bitcoin::Address,
    key_p2: bitcoin::Address,
    key_p2_pk: Clause,
    opt: T,
    timeout: AnyAbsTimeLock,
}
impl<T> Contract for ExpiringOption<T>
where
    GenericBet: TryFrom<T, Error = CompilationError>,
    T: Clone + 'static,
{
    declare!(then, Self::expires, Self::strikes);
    declare!(non updatable);
}

impl<T> ExpiringOption<T> {
    /// Party Two is the option holder
    #[guard]
    fn signed(self, _ctx: Context) {
        self.key_p2_pk.clone()
    }
}

Then we’ll implement the functions:

impl<T> Expires for ExpiringOption<T>
where
    GenericBet: TryFrom<T, Error = CompilationError>,
    T: Clone,
{
    #[then]
    fn expires(self, ctx: sapio::Context) {
        // return the money to each party
        ctx.template()
            .add_output(
                self.party_one.into(),
                &Compiled::from_address(self.key_p1.clone(), None),
                None,
            )?
            .add_output(
                self.party_two.into(),
                &Compiled::from_address(self.key_p2.clone(), None),
                None,
            )?
            .set_lock_time(self.timeout)?
            .into()
    }
    /// Only party 2 can strike!
    #[then(guarded_by = "[Self::signed]")]
    fn strikes(self, ctx: sapio::Context) {
        // Send the money to a generic bet...
        ctx.template()
            .add_output(
                (self.party_one + self.party_two).into(),
                &GenericBet::try_from(self.opt.clone())?,
                None,
            )?
            .into()
    }
}

Now we’ll implement similar logic, but where the amount from party_two is not paid until the strike is called:

/// Similar to `ExpiringOption` except that the option requires an additional
/// value amount to be paid in in order to execute, hence being "under funded"
pub struct UnderFundedExpiringOption<T: 'static> {
    party_one: Amount,
    party_two: Amount,
    key_p1: bitcoin::Address,
    opt: T,
    timeout: AnyAbsTimeLock,
}
impl<T> Contract for UnderFundedExpiringOption<T>
where
    GenericBet: TryFrom<T, Error = CompilationError>,
    T: Clone + 'static,
{
    declare!(then, Self::expires, Self::strikes);
    declare!(non updatable);
}



impl<T> Expires for UnderFundedExpiringOption<T>
where
    GenericBet: TryFrom<T, Error = CompilationError>,
    T: Clone,
{
    #[then]
    fn expires(self, ctx: sapio::Context) {
        ctx.template()
            .add_output(
                self.party_one.into(),
                &Compiled::from_address(self.key_p1.clone(), None),
                None,
            )?
            .set_lock_time(self.timeout)?
            .into()
    }

    #[then]
    fn strikes(self, ctx: sapio::Context) {
        ctx.template()
            .add_amount(self.party_two)
            .add_sequence()
            .add_output(
                (self.party_one + self.party_two).into(),
                &GenericBet::try_from(self.opt.clone())?,
                None,
            )?
            .into()
    }
}

NFT Exercises for the reader:

  • Question: Why isn’t a normal NFT Sale contract an Option?
  • Answer: Because it doesn’t guarantee the uniqueness of the right to purchase
  • Question: How can we implement NFT Options?
  • Answer: The NFT Option has to be the only owner of the NFT. Without writing any new contracts… generate the code to transfer the NFT to a 2-2 multisig between option holder and seller, and pre-sign a timelocked transfer back to the original owner plus a non-timelocked sale to the purchaser for a price. With new contracts? Do the same thing, but without having to stitch it together.
  • Question: Can you do call options? What about put options?
  • Answer: Sure! For a call option have the contract have the NFT in it. For a put option, require that the NFT be put in and the funds present, and pre-sign the transfer. The tricky thing is that if you wish to ‘move’ your NFT while you have a put option open, you must get your counterparty to agree to the new UTXO representing the NFT. But they can validate this client side and sign automatically.

Does this NEED CTV?

No, not in particular. Most of this stuff could be done with online signer server federation between you and counterparty. CTV makes some stuff nicer though, and opens up new possibilities for opening these contracts unilaterally.

Representing Positions as NFTs

Offers to open up a contract could be represented as NFTs! You don’t even need to create the NFT, just bind the NFT interface with an option open as a generic minting parameter, and then you can do price discovery of Option contracts through a dutch auction you thought was just for selling cat pics.

Wen LN?

Well, if you note that we can coop close options and derivatives, and that I claimed we don’t need CTV, these two facts imply that you can put these kinds of contracts inside of the LN no problem :).

What About PowSwap?

I mentioned powswap.com earlier. But you’ll have to wait to read about it, that’s all for today!

  1. I don’t actually have any paid shills, contrary to some people’s beliefs. 



NFTs Part Two: Auctions, Royalties, Mints, Generative, Game Items

Day 22: Rubin's Bitcoin Advent Calendar

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:

  1. We’d see how to do royalties in a sale
  2. We’d see how to do a Dutch auction
  3. We’d see how to do batch mints
  4. We’d see how to make generative art

and one thing I didn’t

  1. 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:

  1. A key for the creator
  2. 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 :)



Packaging Sapio Applications

Day 21: Rubin's Bitcoin Advent Calendar

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?


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.

  1. pun certainly intended. 


© 2011-2021 Jeremy Rubin. All rights reserved.