Here you'll find an assorted mix of content from yours truly. I post about a lot
of things, but primarily
Day 21: Rubin's Bitcoin Advent Calendar
18 Dec 2021
Welcome to day 21 of my Bitcoin Advent Calendar. You can see an index of all
the posts here or subscribe at
judica.org/join to get new posts in your inbox
Today’s a bit of a cheat day for me – not really “new” content, but mostly
stuff re-packaged from learn.sapio-lang.org.
But it belongs in the series, and is it really plagarism if I wrote it myself?
So you’ve written a Sapio contract and you’re ready to get it out into the
world.
How should you release it? How should you use it?
Today’s post covers various ways to deploy and use Sapio contracts.
Note on Open Sourcing:
In general, it is important to make the code available in an open source way,
so others can integrate and use your contracts. Rust’s crates
system provides a natural place to publish for the time being, although
in the future we may build a Sapio specific package manager as smart contracts
have some unique differences.
Packaging Contracts via WASM
WASM is “WebAssembly”, or a standard for producing bytecode objects that can
be run on any platform. As the name suggests, it was originally designed for
use in web browsers as a compiler target for any language to produce code to
run safely from untrusted sources.
So what’s it doing in Sapio?
WASM is designed to be cross platform and deterministic, which makes it a great
target for smart contracts that we want to be able to be reproduced locally. The
determinism also enables our update system. It also makes it relatively safe
to run smart contracts provided by untrusted parties as the security of the WASM
sandbox prevents bad code from harming or infecting our system.
Sapio Contract objects can be built into WASM binaries very easily. The code required is basically:
/// MyContract must support Deserialize and JsonSchema
#[derive(Deserialize, JsonSchema)]
struct MyContract;
impl Contract for MyContract{\*...*\};
/// binds to the plugin interface -- only one REGISTER macro permitted per project
REGISTER![MyContract];
See the example for more details.
The best way to make a new plugin is just to copy that directory and update the Cargo.toml
with a new name.
These compiled objects require a special environment to be interacted with.
That environment is provided by the Sapio CLI as a standalone binary. It is also
possible to use the interface provided by the sapio-wasm-plugin
crate to load
a plugin from any rust codebase programmatically. Lastly, one could create
similar bindings for another platform as long as a WASM interpreter is
available.
Cross Module Calls (CMC)
The WASM Plugin Handle architecture permits one WASM plugin to call into
another. This is incredibly powerful. What this enables one to do is to
package Sapio contracts that are generic and can call one another either by
hash (with effective subresource integrity) or by a nickname (providing easy
user customizability).
For example, suppose I was writing a standard contract component C
which I
publish. Then later, I develop a contract B
which is designed to work with
C
. Rather than having to depend on C
’s source code (which I may not want to
do for various reasons – for example C
could be a standard), I could simply
hard code C
’s hash into B
and call create_contract_by_key(key: &[u8; 32],
args: Value, amt: Amount)
to get the desired code. The plugin management system
automatically searches for a contract plugin with that hash, and tries to call
it with the provided JSON arguments. Using create_contract(key:&str,
args:Value: amt:Amount)
, a nickname can be provided in which case the
appropriate plugin is resolved by the environment. Lastly, it’s possible to use
lookup_this_module_name()
to resolve the currently executing modules hash for
recursive calls. Recursive CMC calls can be helpful when you want to either
make a contract generic, or you want a clean JSON argument interface between
units. It’s also possible for a contract to detect if a generic argument
would result in a recursive CMC and cut-through it locally.
struct C;
const DEPENDS_ON_MODULE : [u8; 32] = [0;32];
impl Contract for C {
#[then]
fn demo(self, ctx: Context) {
let amt = ctx.funds()/2;
ctx.template()
.add_output(amt, &create_contract("users_cold_storage", /**/, amt), None)?
.add_output(amt, &create_contract_by_key(&DEPENDS_ON_MODULE, /**/, amt), None)?
.add_output(amt, &create_contract_by_key(&lookup_this_module_name().unwrap(), /**/, amt), None)?
.into()
}
}
Typed Calls
Using JSONSchemas, plugins have a basic type system that enables run-time
checking for compatibility. Plugins can guarantee they implement particular
interfaces faithfully. These interfaces currently only support protecting the
call, but make no assurances about the returned value or potential errors from
the callee’s implementation of the trait.
For example, suppose I want to be able to specify a provided module must
statisfy a calling convention for batching. I define the trait
BatchingTraitVersion0_1_1
as follows:
/// A payment to a specific address
#[derive(JsonSchema, Serialize, Deserialize, Clone)]
pub struct Payment {
/// The amount to send in sats
pub amount: AmountU64,
/// # Address
/// The Address to send to
pub address: Address,
}
#[derive(Serialize, JsonSchema, Deserialize, Clone)]
pub struct BatchingTraitVersion0_1_1 {
pub payments: Vec<Payment>,
pub feerate_per_byte: AmountU64,
}
I can then turn this into a SapioJSONTrait by implementing the trait and
providing an “example” function.
impl SapioJSONTrait for BatchingTraitVersion0_1_1 {
/// required to implement
fn get_example_for_api_checking() -> Value {
#[derive(Serialize)]
enum Versions {
BatchingTraitVersion0_1_1(BatchingTraitVersion0_1_1),
}
serde_json::to_value(Versions::BatchingTraitVersion0_1_1(
BatchingTraitVersion0_1_1 {
payments: vec![],
feerate_per_byte: Amount::from_sat(0).into(),
},
))
.unwrap()
}
/// optionally, this method may be overridden directly for more advanced type checking.
fn check_trait_implemented(api: &dyn SapioAPIHandle) -> bool {
Self::check_trait_implemented_inner(api).is_ok()
}
}
If a contract module can receive the example, then it is considered to have
implemented the API. We can implement the receivers for a module as follows:
struct MockContract;
/// # Different Calling Conventions to create a Treepay
#[derive(Serialize, Deserialize, JsonSchema)]
enum Versions {
/// # Base
Base(MockContract),
/// # Batching Trait API
BatchingTraitVersion0_1_1(BatchingTraitVersion0_1_1),
}
impl From<BatchingTraitVersion0_1_1> for MockContract {
fn from(args: BatchingTraitVersion0_1_1) -> Self {
MockContract
}
}
impl From<Versions> for TreePay {
fn from(v: Versions) -> TreePay {
match v {
Versions::Base(v) => v,
Versions::BatchingTraitVersion0_1_1(v) => v.into(),
}
}
}
REGISTER![[MockContract, Versions], "logo.png"];
Now MockContract
can be called via the BatchingTraitVersion0_1_1
trait
interface.
Another module in the future need only have a field
SapioHostAPI<BatchingTraitVersion0_1_1>
. This type verifies at deserialize
time that the provided name or hash key implements the required interface(s).
Future Work on Cross Module Calls
- Gitian Packaging: Using a gitian signed packaging distribution system
would enable a user to set up a web-of-trust setting for their sapio compiler
and enable fetching of sub-resources by hash if they’ve been signed by the
appropriate parties.
- NameSpace Registration: A system to allow people to register names
unambiguously would aid in ensuring no conflicts. For now, we can handle
this using a centralized repo.
- Remote CMC: In some cases, we may want to make a call to a remote
server that will call a given module for us. This might be desirable if the
server holds sensitive material that we shouldn’t have.
- Polymorphic CMC: currently, CMC’s only return the
Compiled
type. Perhaps
future CMC
support can return arbitrary types, allowing other types of
functionality to be packaged. For example, it would be great if a guard
clause
could be generated just from a separate WASM module.
What if I don’t want WASM?
Well, ngmi. JK. Kinda.
You do really want WASM. You very much want your contracts to be
deterministically compiled. If they are not, then a lot of things are not
guaranteed to work correctly and you might lose funds.
We’re very focused on run-in WASM and not focused on other things.
That said, Sapio is just a Rust library, so you can embed your contracts
into an application directly, e.g., for an embedded signing device.
If you do this it is paramount that you carefully audit and check that you are
able to get consistent deterministic results out, or that you do not need to be
able to deterministically recompile (this is true in many cases!) and can save
the compilation result.
Another technique you can use is to build a bigger application around a contract
and then compile that to a WASM blob. Also works fine if you’re careful not
to accidentally add some entropy.
That’s all folks. In sum: Sapio is using WASM, you can choose to not use it at
your own peril.
Day 20: Rubin's Bitcoin Advent Calendar
17 Dec 2021
Welcome to day 20 of my Bitcoin Advent Calendar. You can see an index of all
the posts here or subscribe at
judica.org/join to get new posts in your inbox
Today’s post is going to be a bit lighter weight than yesterday’s. We’ll cover
some high level concepts around oracles and then look at some Sapio.
The genesis of this line of inquiry was a conversation with Robin Linus that led to
a pretty cool whitepaper, so definitely
read that if you find this post compelling.
Oracles
Oracles are cool! The most basic form of an useful bitcoin oracle is just a
signing key that signs transactions or reveals information that it “should”
according to some rule.
Protocols for oracles like discrete log contract oracles produce more generic
“key material reveals”, that are more similar to releasing information that
allows counterparties to decrypt the relevant signature.
One of the problems with oracles is that they can equivocate, that is, sign
multiple conflicting statements. It would be nice if we could esnure that they
would be consistent, no?
Bonded Oracles
In order to make the oracles consistent, what we can do is set up our oracles
such that if the oracle ever signs two statements they reveal their private key
to the world. The common way that this is done is via nonce reuse, which is
essentially a way that you can extract a private key from a signature on
messages m1 and m2 using the same nonce r1.
While revealing a key might be punishment enough, we can do one better. We can
require that if a nonce is leaked, meaning some statement was equivocated, then
a some bitcoin protected by that key can be ‘stolen’ by anyone.
But this form is a little problematic, for a few reasons. Reason one is that the
oracle could cancel their bond and take it back while there are still contracts
settling with their data that they then equivocate on.
The other issue is that the funds in the punishment could be claimed by anyone,
including a miner or the oracle themselves, and especially if oracles are also miners!
To fix the first issue, we need to lock up the fund for e.g. 2 weeks and only
use the oracle for the first week to permit 1 week gap in closing. This creates a
new issue that bonds are always expiring, but maybe that’s OK.
To fix the second issue, we need a way of restricting where the funds go to definitely
be out of reach of any bad guys, e.g. burned.
CTV Fixes This.
If you had checktemplateverify, you could stipulate that a
bonded oracle must initiate a bond redemption on chain, at which point anyone
can challenge it if they know the key and they are guaranteed sufficient time
to post a challenge.
The second fix is that CTV can stipulate that the funds must be burned by
sending to an OP_RETURN, not released to miners (which would be problematic if a
miner was also an oracle).
Now our oracle is ready to sign all sorts of stuff, and we can make sure that
for a given Nonce we never sign two conflicting statements.
DLCs?
We can now use this type of oracle for a DLC protocol. We just create the contract
and then we sign+reveal using our staking key whatever messages are required. Any cheating,
and anyone who detects it can burn our money.
Attestation Chains
One of the other cools things we can do with our Bonded oracle is to sign a chain of
attestations.
For example, we could sign message 1, and then sign message 2, and then sign message 3.
We can turn this into a “blockchain” of sorts if when we sign m2 we include a
hash of m1, and when we sign m3 we include a hash of m3.
But we can go a step further. If we’re careful, we can set it up so that
‘branching’ on any message in the chain (by equivocating/producing a conflicting
statement) leaks the key of the bonded oracle with a trick I (think?) I came up
with. Here’s roughly how it works:
message 1: INIT with PK K, nonce R1 for m2, 1 BTC at risk in output X
message 2: SIGN with K, R1 H(m1), nonce R2 for m3
message 3: SIGN with K, R2 H(m1), nonce R3 for m4
If the oracle were to ever branch, it would look like this:
message 1: INIT with PK K, nonce R1, 1 BTC at risk in output X
message 2: SIGN with K, R1 H(m1), nonce R2
message 3: SIGN with K, R2 H(m2), nonce R3
message 3': SIGN with K, R2 H(m3), nonce R3'
The leak would be able to extract K’s secret key via the reuse of R2.
While it might seem that you could ‘get away with it’, because we verify at each
step that the last used nonce was from the prior step it cannot be forged. The
commitment to H(mi)
also makes it more difficult for an invalid signature to
float around since from just the top you can know what all the other states
should be.
Proof of Stake?
Essentially we’ve built a system for proof-of-stake on Bitcoin. Imagine you have
100BTC locked up in these contracts across 127 instances, and you want to run
some system based on it.
You can just download the message signed at state Mn and see what the majority
of signers voted for that slot.
Any signer who cheats gets their funds burned, and you’d learn to exclude them
from consensus.
If you do need to have a ‘rollback’, you can do it by engineering your protocol
to allow new updates to the chain of signatures to produce a rollback.
Partial Slashing
You can even implement partial slashing. Suppose you have 10 coins in a contract under
key K1. If a cheat is detected, it authorizes a txn which burns 2 and puts the
remaining 8 into key K2. The next round of slashing could put 6.4 under K3.
Alternatives to Burning
Burning sats is sad. What if instead of a burn, coins went into an annuity that
would be claimable 100 years from now? That way, no economic agents around today
can plan to cheat and capture the value of it, but the burned coins can serve a
real function. While this is slightly less secure than a full burn, it’s also
more secure since it creates an incentive to continue to build the chain.
Or donate to a well known chairty address/developer fund :p
Implementing a Staked Signer
To begin, we’ll define some ‘type tags’. This is a technique in rust where we
define empty structs that let us build a little state machine in the type
system. You can read more on the technique
here.
/// # Operational State
/// State where stakes should be recognized for voting
#[derive(JsonSchema, Deserialize)]
pub struct Operational;
/// # Closing State
/// State where stakes are closing and waiting evidence of misbehavior
#[derive(JsonSchema, Deserialize)]
struct Closing;
/// # Staking States (Operational, Closing)
/// enum trait for states
pub trait StakingState {}
impl StakingState for Operational {}
impl StakingState for Closing {}
Next, we’ll define an interface that an implementation of a Staked Signer should
implement:
By default something that is declared is given a default not-present implementation.
/// Functional Interface for Staking Contracts
pub trait StakerInterface
where
Self: Sized,
{
decl_guard!(
/// The key used to sign messages
staking_key
);
decl_guard!(
/// the clause to begin a close process
begin_redeem_key
);
decl_guard!(
/// the clause to finish a close process
finish_redeem_key
);
decl_then!(
/// The transition from Operational to Closing
begin_redeem
);
/// Why would anyone ever cheat!!
#[then(guarded_by = "[Self::staking_key]")]
fn cheated(self, ctx: sapio::Context) {
let f = ctx.funds();
ctx.template()
// commit to metadata here for convenience, but really could be anywhere!
// exercise for reader: what if we plugged in another instance of StakerInterface
// that:
// 1. switches to a new, unburned key
// 2. pays 80% to the new StakerInterface
// 3. pays 20% to an annuity that pays miners over e.g. 1000 blocks
// at some point in the far future.
.add_output(f, &Compiled::from_op_return(&self.data.as_inner()[..])?, None)?
.into()
}
}
/// We can delcare the Contract impl for all valid Staker<T>
impl<T: 'static + StakingState> Contract for Staker<T>
where
Staker<T>: StakerInterface,
T: StakingState,
{
declare! {then, Self::begin_redeem, Self::cheated}
declare! {finish, Self::finish_redeem_key}
declare! {non updatable}
}
Next, we’ll define the data required for our staker:
/// # Staker: A Bonded Signing Contract
/// Staker is a contract that proceeds from Operational -> Closing
/// During it's lifetime, many things can be signed with signing_key,
/// but should the key ever leak (e.g., via nonce reuse) the bonded
/// funds can be burned.
///
/// Burning is important v.s. miner fee because otherwise the staker
/// can bribe (or be a miner themselves) to cheat.
#[derive(JsonSchema, Deserialize)]
pub struct Staker<T: StakingState> {
/// # Timeout
/// How long to wait for evidence after closing
timeout: AnyRelTimeLock,
/// # Signing Key
/// The key that if leaked can burn funds
signing_key: PublicKey,
/// # Redemption Key
/// The key that will be used to control & return the redeemed funds
redeeming_key: PublicKey,
/// # Data
/// Arbitrary hash of metadata that is needed to start the attestation chain
data: sha256::Hash,
/// current contract state.
#[serde(skip, default)]
state: PhantomData<T>,
}
Next, we’ll define the StakerInterface when our channel is operational. At this phase,
funds can either be burnt or the redeeming key can start the process of withdrawing.
impl StakerInterface for Staker<Operational> {
/// redeeming key
#[guard]
fn begin_redeem_key(self, _ctx: Context) {
Clause::Key(self.redeeming_key)
}
/// begin redemption process
#[then(guarded_by = "[Self::begin_redeem_key]")]
fn begin_redeem(self, ctx: sapio::Context) {
let f = ctx.funds();
ctx.template()
.add_output(
f,
&Staker::<Closing> {
state: Default::default(),
timeout: self.timeout,
signing_key: self.signing_key,
redeeming_key: self.redeeming_key,
},
None,
)?
.into()
}
/// staking key
#[guard]
fn staking_key(self, _ctx: Context) {
Clause::Key(self.signing_key)
}
}
Lastly, for closing we should not be able to “loop” back into Closing or
Operational, so we do not implement the begin_redeem
logic.
impl StakerInterface for Staker<Closing> {
#[guard]
fn finish_redeem_key(self, _ctx: Context) {
Clause::And(vec![Clause::Key(self.redeeming_key), self.timeout.into()])
}
#[guard]
fn staking_key(self, _ctx: Context) {
Clause::Key(self.signing_key)
}
}
Attestation Chain
In order to start the attestation chain, the data
field should be the hash of something like:
struct AttestationStart {
/// # Nonce
/// a nonce element
first_nonce: [0u8; 32],
/// # Key
/// the key to sign with (for convenience, should match the StakedSigner's
/// staking key)
key: PublicKey,
/// # Purpose
/// useful to have some sort of description (machine readable) of what this attestor
/// is signing for
purpose: Vec<u8>
}
To start using the attestation chain, we build a linked list of Attest
signatures as described below:
enum Either<T, U> {
Left(T),
Right(U)
}
struct Attest {
/// # Signature
/// the signature over the below data fields
sig: Signature,
/// # Message
/// whatever info the protocol expects to be signed
message: Vec<u8>,
/// # Nonce
/// a nonce element
next_nonce: [0u8; 32],
/// # Height
/// what # signature is this
height: u64,
/// # Previous Attestation
/// the last attestation. we either keep a hash or the actual value
prev: Either<Hash, Either<Box<Attest>, AttestationStart>>
}
It would be possible – but perhaps overkill – to instead encode this structure
as a Sapio contract with continuation
branches. I’ll leave that as an exercise
for the reader for now!
Galaxy Brain Time
What if we used this staked signer to coordinate a decentralized mining pool
where the stakers sign off on work shares they have seen…
That’s All Folks!
Day 19: Rubin's Bitcoin Advent Calendar
16 Dec 2021
Welcome to day 19 of my Bitcoin Advent Calendar. You can see an index of all
the posts here or subscribe at
judica.org/join to get new posts in your inbox
For today’s post we’re going to build out some Sapio NFT protocols that are
client-side verifiable. Today we’ll focus on code, tomorrow we’ll do more
discussion and showing how they work. I was sick last night (minor burrito
oriented food poisoning suspected) and so I got behind, hence this post being up
late.
As usual, the disclaimer that as I’ve been behind… so we’re less focused today
on correctness and more focused on giving you the shape of the idea. In other
words, I’m almost positive it won’t work properly, but it can compile! And the
general flow looks correct.
There’s also a couple new concepts I want to adopt as I’ve been working on this,
so those are things that will have to happen as I refine this idea to be
production grade.
Before we start, let’s get an eagle-eye view of the ‘system’ we’re going to be
building, because it represents multiple modules and logical components.
By the end, we’ll have 5 separate things:
- An Abstract NFT Interface
- An Abstract Sellable Interface
- An Abstract Sale Interface
- A Concrete Sellable NFT (Simple NFT)
- A Concrete Sale Interface (Simple NFT Sale)
In words:
Simple NFT implements both NFT
and Sellable
, and has a sell
function that
can be called with any Sale module.
Simple NFT Sale implements Sale
, and can be used with the sell
of anything
that implements Sellable
and NFT
.
We can make other implementations of Sale
and NFT
and they should be
compatible.
How’s it going to ‘work’?
Essentially how this is going to work do is
- An artist mint an NFT.
- The artist can sell it to anyone whose bids the artist accepts
Normally, in Ethereum NFTs, you could do something for step 2:
- The artist signs “anyone can buy at this price”
with Bitcoin NFTs, it’s a little different. The artist has to run a server that
accepts bids above the owner’s current price threshold and returns signed
under-funded transaction that would pay the owner the asking price.
Alternatively, the bidder can send an open-bid that the owner can fill
immediately.
Because Sapio is super-duper cool, we can make abstract interfaces for this
stuff so that NFTs can have lots of neat features like enforcing royalties,
dutch auction prices, batch minting, generative art minting, and more. We’ll see
a bit more tomorrow.
Client validation is central to this story. A lot of the rules are not
enforced by the Bitcoin blockchain. They are, however, enforced by requiring
that the ‘auditor’ be able to re-reach the same identical contract state by
re-compiling the entire contract from the start. I.e., as long as you generate
all your state transitions through Sapio, you can verify that an NFT is
‘authentic’. Of course, anyone can ‘burn’ an NFT if they want by sending e.g.
to an unknown key. Client side validation just posits that sending to an
unknown key is ‘on the same level’ of error as corrupting an NFT by doing state
transitions without having the corresponding ‘witness’ of sapio effects to
generate the transfer.
Please re-read this section after you get throught the code (I’ll remind you).
Declaring an NFT Minting Interface
First we are going to declare the basic information for a NFT.
Every NFT should have a owner (PublicKey) and a locator (some url, IPFS hash,
etc).
NFTs also should track which Sapio module was used to mint them, to ensure
compatibility going forward. If it’s not known, modules can try to fill it in
and guess (e.g., a good gues is “this module”).
Let’s put that to code:
/// # Trait for a Mintable NFT
#[derive(Serialize, JsonSchema, Deserialize, Clone)]
pub struct Mint_NFT_Trait_Version_0_1_0 {
/// # Initial Owner
/// The key that will own this NFT
pub owner: bitcoin::PublicKey,
/// # Locator
/// A piece of information that will instruct us where the NFT can be
/// downloaded -- e.g. an IPFs Hash
pub locator: String,
/// # Minting Module
/// If a specific sub-module is to be used / known -- when in doubt, should
/// be None.
pub minting_module: Option<SapioHostAPI<Mint_NFT_Trait_Version_0_1_0>>,
}
/// Boilerplate for the Mint trait
pub mod mint_impl {
use super::*;
#[derive(Serialize, Deserialize, JsonSchema)]
pub enum Versions {
Mint_NFT_Trait_Version_0_1_0(Mint_NFT_Trait_Version_0_1_0),
}
/// we must provide an example!
impl SapioJSONTrait for Mint_NFT_Trait_Version_0_1_0 {
fn get_example_for_api_checking() -> Value {
let key = "02996fe4ed5943b281ca8cac92b2d0761f36cc735820579da355b737fb94b828fa";
let ipfs_hash = "bafkreig7r2tdlwqxzlwnd7aqhkkvzjqv53oyrkfnhksijkvmc6k57uqk6a";
serde_json::to_value(mint_impl::Versions::Mint_NFT_Trait_Version_0_1_0(
Mint_NFT_Trait_Version_0_1_0 {
owner: bitcoin::PublicKey::from_str(key).unwrap(),
locator: ipfs_hash.into(),
minting_module: None,
},
))
.unwrap()
}
}
}
Shaweeeeet! We have an NFT Minting Interface!
But you can’t actually use it to Mint yet, since we lack an Implementation.
Before we implement it…
What are NFTs Good For? Selling! (Sales Interface)
If you have an NFT, you probably will want to sell it in the future. Let’s
declare a sales interface.
To sell an NFT we need to know:
- Who currently owns it
- Who is buying it
- What they are paying for it
- Maybe some extra stuff
/// # NFT Sale Trait
/// A trait for coordinating a sale of an NFT
#[derive(Serialize, JsonSchema, Deserialize, Clone)]
pub struct NFT_Sale_Trait_Version_0_1_0 {
/// # Owner
/// The key that will own this NFT
pub sell_to: bitcoin::PublicKey,
/// # Price
/// The price in Sats
pub price: AmountU64,
/// # NFT
/// The NFT's Current Info
pub data: Mint_NFT_Trait_Version_0_1_0,
/// # Extra Information
/// Extra information required by this contract, if any.
/// Must be Optional for consumer or typechecking will fail.
/// Usually None unless you know better!
pub extra: Option<Value>,
}
/// Boilerplate for the Sale trait
pub mod sale_impl {
use super::*;
#[derive(Serialize, Deserialize, JsonSchema)]
pub enum Versions {
/// # Batching Trait API
NFT_Sale_Trait_Version_0_1_0(NFT_Sale_Trait_Version_0_1_0),
}
impl SapioJSONTrait for NFT_Sale_Trait_Version_0_1_0 {
fn get_example_for_api_checking() -> Value {
let key = "02996fe4ed5943b281ca8cac92b2d0761f36cc735820579da355b737fb94b828fa";
let ipfs_hash = "bafkreig7r2tdlwqxzlwnd7aqhkkvzjqv53oyrkfnhksijkvmc6k57uqk6a";
serde_json::to_value(sale_impl::Versions::NFT_Sale_Trait_Version_0_1_0(
NFT_Sale_Trait_Version_0_1_0 {
sell_to: bitcoin::PublicKey::from_str(key).unwrap(),
price: Amount::from_sat(0).into(),
data: Mint_NFT_Trait_Version_0_1_0 {
owner: bitcoin::PublicKey::from_str(key).unwrap(),
locator: ipfs_hash.into(),
minting_module: None,
},
extra: None,
},
))
.unwrap()
}
}
}
That’s the interface for the contract that sells the NFTs. We also need an
interface for NFTs that want to initiate a sale.
To do that, we need to know:
- What kind of sale we are doing
- The data for that sale
This is really just expressing that we need to bind a NFT Sale Implementation to
our contract. We can express the sale interface as follows.
/// # Sellable NFT Function
/// If a NFT should be sellable, it should have this trait implemented.
pub trait SellableNFT: Contract {
decl_continuation! {<web={}> sell<Sell>}
}
/// # Sell Instructions
#[derive(Serialize, Deserialize, JsonSchema)]
pub enum Sell {
/// # Hold
/// Don't transfer this NFT
Hold,
/// # MakeSale
/// Transfer this NFT
MakeSale {
/// # Which Sale Contract to use?
/// Specify a hash/name for a contract to generate the sale with.
which_sale: SapioHostAPI<NFT_Sale_Trait_Version_0_1_0>,
/// # The information needed to create the sale
sale_info: NFT_Sale_Trait_Version_0_1_0,
},
}
impl Default for Sell {
fn default() -> Sell {
Sell::Hold
}
}
impl StatefulArgumentsTrait for Sell {}
Getting Concrete: Making an NFT
Let’s create a really simple NFT now that implements these interfaces.
There’s a bit of boilerplate, so we’ll go section-by-section.
First, let’s declare the SimpleNFT
/// # SimpleNFT
/// A really simple NFT... not much too it!
#[derive(JsonSchema, Serialize, Deserialize)]
pub struct SimpleNFT {
/// The minting data, and nothing else.
data: Mint_NFT_Trait_Version_0_1_0,
}
/// # The SimpleNFT Contract
impl Contract for SimpleNFT {
// NFTs... only good for selling?
declare! {updatable<Sell>, Self::sell}
// embeds metadata
declare! {then, Self::metadata_txns}
}
First, let’s implement the logic for selling the NFT… You remember our old
friend the Sales interface?
impl SimpleNFT {
/// # signed
/// Get the current owners signature.
#[guard]
fn signed(self, ctx: Context) {
Clause::Key(self.data.owner.clone())
}
}
fn default_coerce(k: <SimpleNFT as Contract>::StatefulArguments) -> Result<Sell, CompilationError> {
Ok(k)
}
impl SellableNFT for SimpleNFT {
#[continuation(guarded_by = "[Self::signed]", web_api, coerce_args = "default_coerce")]
fn sell(self, ctx: Context, sale: Sell) {
if let Sell::MakeSale {
sale_info,
which_sale,
} = sale
{
// if we're selling...
if sale_info.data.owner != self.data.owner {
// Hmmm... metadata mismatch! the current owner does not
// matched the sale's claimed owner.
return Err(CompilationError::TerminateCompilation);
}
// create a contract from the sale API passed in
let compiled = Ok(CreateArgs {
context: ContextualArguments {
amount: ctx.funds(),
network: ctx.network,
effects: unsafe { ctx.get_effects_internal() }.as_ref().clone(),
},
arguments: sale_impl::Versions::NFT_Sale_Trait_Version_0_1_0(sale_info.clone()),
})
.map(serde_json::to_value)
// use the sale API we passed in
.map(|args| create_contract_by_key(&which_sale.key, args, Amount::from_sat(0)))
// handle errors...
.map_err(|_| CompilationError::TerminateCompilation)?
.ok_or(CompilationError::TerminateCompilation)?;
// send to this sale!
let mut builder = ctx.template();
// todo: we need to cut-through the compiled contract address, but this
// upgrade to Sapio semantics will come Soon™.
builder = builder.add_output(compiled.amount_range.max(), &compiled, None)?;
builder.into()
} else {
/// Don't do anything if we're holding!
empty()
}
}
}
Next, let’s implement the metadata logic. There are a million ways to do metadata,
so feel free to ‘skip’ this section and just let your mind wander on interesting
things you could do here…
impl SimpleNFT {
/// # unspendable
/// what? This is just a sneaky way of making a provably unspendable branch
/// (since the preimage of [0u8; 32] hash can never be found). We use that to
/// help us embed metadata inside of our contract...
#[guard]
fn unspendable(self, ctx: Context) {
Clause::Sha256(sha256::Hash::from_inner([0u8; 32]))
}
/// # Metadata TXNs
/// This metadata TXN is provably unspendable because it is guarded
/// by `Self::unspendable`. Neat!
/// Here, we simple embed a OP_RETURN.
/// But you could imagine tracking (& client side validating)
/// an entire tree of transactions based on state transitions with these
/// transactions... in a future post, we'll see more!
#[then(guarded_by = "[Self::unspendable]")]
fn metadata_txns(self, ctx: Context) {
ctx.template()
.add_output(
Amount::ZERO,
&Compiled::from_op_return(
&sha256::Hash::hash(&self.data.locator.as_bytes()).as_inner()[..],
)?,
None,
)?
// note: what if we also comitted to the hash of the wasm module
// compiling this contract?
.into()
}
}
Lastly, some icky boilerplate stuff:
#[derive(Serialize, Deserialize, JsonSchema)]
enum Versions {
Mint_NFT_Trait_Version_0_1_0(Mint_NFT_Trait_Version_0_1_0),
}
impl TryFrom<Versions> for SimpleNFT {
type Error = CompilationError;
fn try_from(v: Versions) -> Result<Self, Self::Error> {
let Versions::Mint_NFT_Trait_Version_0_1_0(mut data) = v;
let this = LookupFrom::This
.try_into()
.map_err(|_| CompilationError::TerminateCompilation)?;
match data.minting_module {
// if no module is provided, it must be this module!
None => {
data.minting_module = Some(this);
Ok(SimpleNFT { data })
}
// if a module is provided, we have no idea what to do...
// unless the module is this module itself!
Some(ref module) if module.key == this.key => Ok(SimpleNFT { data }),
_ => Err(CompilationError::TerminateCompilation),
}
}
}
REGISTER![[SimpleNFT, Versions], "logo.png"];
Right on! Now we have made a NFT Implementation. We can Mint one, but wait.
How do we sell it?
We need a NFT Sale Implementation
So let’s do it. In today’s post, we’ll implement the most boring lame ass Sale…
Tomorrow we’ll do more fun stuff, I swear.
First, let’s get our boring declarations out of the way:
/// # Simple NFT Sale
/// A Sale which simply transfers the NFT for a fixed price.
#[derive(JsonSchema, Serialize, Deserialize)]
pub struct SimpleNFTSale(NFT_Sale_Trait_Version_0_1_0);
/// # Versions Trait Wrapper
#[derive(Serialize, Deserialize, JsonSchema)]
enum Versions {
/// # Batching Trait API
NFT_Sale_Trait_Version_0_1_0(NFT_Sale_Trait_Version_0_1_0),
}
impl Contract for SimpleNFTSale {
declare! {updatable<()>, Self::transfer}
}
fn default_coerce<T>(_: T) -> Result<(), CompilationError> {
Ok(())
}
impl From<Versions> for SimpleNFTSale {
fn from(v: Versions) -> SimpleNFTSale {
let Versions::NFT_Sale_Trait_Version_0_1_0(x) = v;
SimpleNFTSale(x)
}
}
REGISTER![[SimpleNFTSale, Versions], "logo.png"];
Now, onto the logic of a sale!
impl SimpleNFTSale {
/// # signed
/// sales must be signed by the current owner
#[guard]
fn signed(self, ctx: Context) {
Clause::Key(self.0.data.owner.clone())
}
/// # transfer
/// transfer exchanges the NFT for cold hard Bitcoinz
#[continuation(guarded_by = "[Self::signed]", web_api, coerce_args = "default_coerce")]
fn transfer(self, ctx: Context, u: ()) {
let amt = ctx.funds();
// first, let's get the module that should be used to 're-mint' this NFT
// to the new owner
let key = self
.0
.data
.minting_module
.clone()
.ok_or(CompilationError::TerminateCompilation)?
.key;
// let's make a copy of the old nft metadata..
let mut mint_data = self.0.data.clone();
// and change the owner to the buyer
mint_data.owner = self.0.sell_to;
// let's now compile a new 'mint' of the NFT
let new_nft_contract = Ok(CreateArgs {
context: ContextualArguments {
amount: ctx.funds(),
network: ctx.network,
effects: unsafe { ctx.get_effects_internal() }.as_ref().clone(),
},
arguments: mint_impl::Versions::Mint_NFT_Trait_Version_0_1_0(mint_data),
})
.and_then(serde_json::to_value)
.map(|args| create_contract_by_key(&key, args, Amount::from_sat(0)))
.map_err(|_| CompilationError::TerminateCompilation)?
.ok_or(CompilationError::TerminateCompilation)?;
// Now for the magic:
// This is a transaction that creates at output 0 the new nft for the
// person, and must add another input that pays sufficiently to pay the
// prior owner an amount.
// todo: we also could use cut-through here once implemented
// todo: change seem problematic here? with a bit of work, we could handle it
// cleanly if the buyer identifys an output they are spending before requesting
// a purchase.
ctx.template()
.add_output(amt, &new_nft_contract, None)?
.add_amount(self.0.price.into())
.add_sequence()
.add_output(self.0.price.into(), &self.0.data.owner, None)?
// note: what would happen if we had another output that
// had a percentage-of-sale royalty to some creator's key?
.into()
}
}
And that’s it! Makes sense, right? I hope…
But if not
Re read the part before the code again! Maybe it will be more clear now :)
Day 18: Rubin's Bitcoin Advent Calendar
15 Dec 2021
Welcome to day 18 of my Bitcoin Advent Calendar. You can see an index of all
the posts here or subscribe at
judica.org/join to get new posts in your inbox
Today’s post will be a pretty different format that usual, it’s basically going
to be a pictorial walk through of the Sapio
Studio, the frontend tool for Sapio
projects. As an example, we’ll go through a Payment Pool contract to familiarize
ourselves.
I wanted to put this post here, before we get into some more applications,
because I want you to start thinking past “cool one-off concepts we can
implement” and to start thinking about reusable components we can build and ship
into a common Bitcoin Smart Contract software (Sapio Studio or its successors).
At it’s core, Sapio Studio is just a wallet frontend to Bitcoin Core.
You can make a transaction, just like normal…
And see it show up in the pending transactions…
And even mine some regtest blocks.
But where Sapio Studio is different is that there is also the ability to create
contracts.
Before we can do that, we need to load a WASM Plugin with a compiled contract.
Let’s load the Payment Pool module. You can see the code for it
here.
And now we can see we have a module!
Let’s load a few more so it doesn’t look lonely.
Now let’s check out the Payment Pool module.
Now let’s check out another one – we can see they each have different types of
arguments, auto-generated from the code.
Let’s fill out the form with 10 keys to make a Payment Pool controlled by 10
people, and then submit it.
What’s that??? It’s a small bug I am fixing :/. Not to worry…
Just click repair layout.
And the presentation resets. I’ll fix it soon, but it can be useful if there’s a
glitch to reset it.
Now we can see the basic structure of the Payment Pool, and how it splits up.
Let’s get a closer look…
Let’s zoom out (not helpful!)…
Let’s zoom back in. Note how the transactions are square boxes and the outputs
are rounded rectangles. Blue lines connect transactions to their outputs. Purple lines
connect outputs to their (potential) spends.
If we click on a transaction we can learn more about it.
We even have some actions that we can take, like sending it to the network.
Let’s try it….
Oops! We need to sign it first…
And then we can send it.
What other buttons do we have? What’s this do?
It teleports us to the output we are creating!
Notice how the output is marked “Updatable”, and there is also a “DO_TX”
button (corresponding to the DO_TX in the Payment Pool). Let’s click that…
Ooooh. It prompts us with a form to do the transaction!
Ok, let’s fill this sucker out…
Click submit, then recompile (separate actions in case we want to make multiple “moves” before recompiling).
I really need to fix this bug…
Voila!
As you can see, the original graph is intact and we’ve augmented onto it the new state transition.
The new part has our 0.1 BTC Spend + the re-creation of the Payment Pool with less funds.
Ok, let’s go nuts and do another state transition off-of the first one? This time more payouts!
Submit…
And Recompile…
I skipped showing you the bug this time.
Now you can see two state transitions! And because we used more payouts than one, we can see some congestion control at work.
It works! It all really, really works!
One more thing I can’t show you with this contract is the timing simulator.
This lets you load up a contract (like our Hanukkiah below) and…
Simulate the passing of time (or blocks).
Pretty cool!
There are also some settings you can configure for display settings, the node,
and for sapio-cli. The first time you run Sapio, you’ll need to get some of
these things configured correctly or else it will be broken. Currently, if you
look here you
can find a template for a script to get everything up and running for a first
shot at it, otherwise you’ll have to do it by hand, or just change your
preferences.json
to be similar to mine in the note.
before you ask…
OF COURSE THERE IS DARK MODE
configured by your local system theme preference
I hope you enjoyed this! There’s a metric fuckload of work still to do to make
Sapio Studio & Sapio anywhere near production grade, but I hope this has helped
elucidate how powerful and cool the Sapio approach is, and has inspired you to
build something and try it out! I’d also be really eager for feedback on what
features should be here/are missing.
Lastly, if you’re just excited about this, it’s definitely a project
that could use more experienced react/electron/bitcoin contributors, either
yourself or if you’re interested in sponsoring :).
Day 17: Rubin's Bitcoin Advent Calendar
14 Dec 2021
Welcome to day 17 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
A short story. I want to tell first. I recently made friends with
Eugene, this really smart 19 year old Cal
dropout, when I was visiting Miami for the NFT bachanal around Art Basel.
Eugene just dropped a project he’s been working on, and it’s really freakin’
cool. He basically implemented a human v. chess engine in Solidity that mints
beautiful interactive NFTs of representations of the contract’s internal states.
You can play the game / mint one for like 0.1 ETH on his site
here, and see a permanent record of my
embarassingly bad move where I missed a mate-in-one here:
Definitely check out the site and read how it’s implemented. Eugene is very
bright, and a talented hacker. The project? It’s not a get-rich-quick project,
only cost is gas, so Eugene’s not raking it in (altho I think he should have,
but he’ll have many more successes).
So what’s the moral of this story?
Why isn’t Eugene working on Bitcoin? People say that “eth is a scam” and that
“everyone working on it are scammers”. But I don’t see that. I see people, like
Eugene, wanting to build cool shit every day. And wanting to ship cool stuff.
It’s fun. And I like to be friends with creative and curious people.
Working on Bitcoin is can be fun. But mostly it’s not. My post yesterday? The
one describing new techniques to make Bitcoin more decentralized? I had a lot
of fun writing it. And then someone claimed that my work is “very dangerous” to
Bitcoin.
I get it, I truly do. Bitcoin is money for enemies. Don’t trust, verify. In
the IRC channels, twitter spaces, and other forums we hear rants like the below
all the time:
Working on Bitcoin isn’t just a stupid fucking game of chess you idiot, it’s
solving literally every geoscale challenge humanity has ever faced and your work
on that is a bajillion fold more worthwhile. You are an asshole thinking you
deserve to have any fun in your life when you could be a miserable piece of shit
with people shouting at you all the time about how you suck and struggling to
ship small features and hoping how you might ship one bigger project this
decade. Fun? Fun??? You fucking asshole. If I don’t kill you, someone else will.
But developers are people too, and good developers like to build cool shit. If
you haven’t noticed, Bitcoin development has a bit of a burnout problem, with
multiple contributors stepping down their engagement recently. A likely cause
is the struggle it takes to ship even the smallest features, not to mention the
monumental effort it takes to ship a single large project. But the death
threats certainly don’t help either.
It doesn’t have to be this hard
It’s hard to tell people, especially younger folk just entering the space, to
work on Bitcoin full-time. What I say is as follows:
If you have a strong ideological calling to the mission of Bitcoin and sound money,
it’s absolutely the most meaningful project for you to work on. But if that’s not you,
and you want to explore crypto, you should probably play around with Ethereum or
something else. Bitcoin is really tough to get funded for and the community,
while amazing, can be very hostile.
If I were more selfish about the mission, I’d glaze over these details. But I
want folks to decide for themselves and find something that makes them truly
happy. For most “at-heart” Bitcoiners that won’t be a disincentive to taking
the orange-pill, but for some it might. Note: I’m Jewish, so that probably
influence my views on converting people to things (Jews believe in discouraging
converts to find the pure of heart).
Despite my best efforts to convince myself otherwise, I’ve been an audit-the-fed
libertarian since elementary school or something and I have photos of myself
with Ron Paul a decade apart. So I am one of those ideologically drawn to
Bitcoin and not other projects with weaker foundations.
But it doesn’t mean I don’t turn an envious eye to the activity and research
happening in other communities, or get excited about on-chain chess engines even
if they’re impractical for now. And there’s also the magic of a supportive
communtiy that doesn’t threaten to have you beaten up when they disagree with
you about the minutae of a soft fork rollout.
Cool technologies attract nerds like moths to a lamp at night. Smart nerds
trying to solve interesting problems create solutions. Experiments that strain
the limits of a platform expose problems and create demand for solutions.
These solutions often have major positive externalities.
I don’t think I’m going to convince you here to care about NFTs. But I am –
hopefully – going to convince you to care about NFTs the phenomenon.
For example, scaling challenge in Ethereum have led to the development of Zero
Knowledge Roll-Ups, privacy issues things like Tornado Cash, and more. While as
a project Eth might be #ngmi, Bitcoiners have traditionally said that if
anything is worth having we’ll just be able to implement it ourselves in
Bitcoin. But there are certain things that have network effects where it will be
hard for us to replicate. And by the time we do go to replicate, the tooling
that’s been developed for doing these things on Eth might be like a decade ahead
of what we’ve got for Bitcoin. And all of the smart kids are going to become
adults who are bought in technically and socially on things other than Bitcoin.
And that really freakin’ matters.
I’m not advocating that Bitcoiners should embrace full-blown degeneracy. But
also it’s not in particular our job to prevent it technically. And the tools that
are produced when people have fun can lead to major innvoations.
For example, right now I am working on a system for building NFTs for Bitcoin on
Sapio.
Why? It’s fun. It touches on almost all of the infrastructure problems I’m
working on for Sapio right now. And it is low enough risk – in terms of risk of
losing Bitcoin – that I feel comfortable building and experimenting with these
NFT test-subjects. Worst-come-to-worst, the artists can always re-issue anything
corrupted via a software flaw. And then as the software matures with low-risk
but still fun applications, we can apply those learnings to managing Bitcoin as
well.
I also want to note that I really like artists. Artists as a community are
incredible. Artists use NFTs, so I like NFTs. Artists are the voice of the
people. Art can tear down the establishment. Art can change the world. And so
for Bitcoin, whose use is an inherently political message, what better community
to engage than the art world?
Bitcoiners are really fixated on the technical nonsense backing NFTs – yeah,
it’s not ‘literally’ the artwork. But then again, what literally is the
artwork? If you want a photograph and you want to pay the photographer for it,
do you? Do you get a receipt for it? Can you use that bill of sale to sell the
photo later? NFTs are just a better that. And if you don’t like the art that’s
sold as NFTs right now, why not find artists you do like? Why not get them
onboarded onto Bitcoin and fully orange-pilled? Hint: the HRF has a whole Art
in Protest section on their website.
Bitcoin NFTs could enable Bitcoin holders to pay dissident artists for their art
and avoid being shut down by authorities. Why not embrace that culturally? Isn’t
that the group that Bitcoin is for? And if you don’t like that, maybe you just
don’t like art. That’s ok, don’t buy it.
But NFTs are Stupid JPEGs Man
Ok sure. Whatever.
But there are all these contracts (as I showed you in previous posts) that
Bitcoin would benefit from, like inheritence schemes, vaults, decentralized
mining, payment pools, and more. Believe it or not, there tools needed to make
NFTs work well are the exact same tools required to make these work seamlessly.
So why not have a little fun, let people experiment with new ideas, get excited,
grow the community, and convert the big innovations into stable and mature
tooling for the critical infrastructure applications? And maybe we’ll uncover
some upside for brand new things that never occured possible to us before.
Throught the end of this series I’ll have some more posts detailing how to build
NFTs, Derivatives, DAOs, and Bonded Oracles. I hope that you can view them with
an open mind and appreciate how – even if you don’t think they are core to what
Bitcoin has to do – these innovations will fuel development of tools to support
the projects you do like without turning Bitcoin into a shitcoin. Who knows,
maybe you’ll find a new application you like.