Here you'll find an assorted mix of content from yours truly. I post about a lot
of things, but primarily
Day 23: Rubin's Bitcoin Advent Calendar
20 Dec 2021
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.” 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:
- selecting carefully parameters a and b
- charging a premium
- 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!
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 :)