This post is syndicated from rubin.io.
Welcome to day 24 of my Bitcoin Advent Calendar. You can see an index of all the posts here or subscribe at judica.org/join to get new posts in your inbox
Today’s post is near and dear to my heart – years ago I put up an interest form for powswap.com, but as I went down the rabbit hole I realized how badly I wanted generic tooling to automate the building of these which is partly what led to Sapio!
So therefore it’s very exciting to show you the basics of powswap in Sapio. You can see how bad the early version was here. If you want to contrast life with Sapio and without.
The basic idea of Powswap is super simple. It is a contract that measures a block surplus or deficit – a Block Delta Contract (BDC). A BDC allows counterparties to bet on statements like “at the end of 6 months, we will be +/- 1000 blocks against the current expected number of blocks”, and program a payoff curve based on the binary outcome of that. The block delta should be – and this is a matter for the analysts to price on and model – correlated with changes in hashrate.
Well imagine you are about to buy a new fancy mining rig to mine with. But you have a moment of doubt – what if everyone else is doing that right now too?
You could buy hashrate derivatives where you win money if the hashrate increases and lose if it stays the same.
This would de-risk your investment in mining.
You can also lever-up and increase profit if you’re adding a lot of hashrate, doubling down that hashrate goes up, but let’s not entertain the degens shall we.
One could imagine making a BDC based on the Oracle system we saw in yesterday’s post. But the magic of Powswap is that we will do this without using any oracle whatsoever, just measuring the blocks directly.
How do we do this?
The answer is actually really simple. Suppose Alice wants to get 1 Bitcoin if 100 blocks are missing at the end of the week (the 28th, let’s say expected 1000 blocks), and Bob wants to win 1 Bitcoin if they are actually there.
All we have to do is have Alice and Bob agree to a multisig to deposit 0.5 BTC each to, and then pre-sign from it two transactions:
Let’s think it through:
Suppose that Alice is right and blocks are 100 short by noon.
In the next 8 hours, only 48 blocks should be mined (and probably less, if the hashrate has actually decreased).
After that point, Alice has 8 more hours (again, probably more if hashrate actually decreased) to broadcast and claim her BTC.
Suppose that Bob is right and blocks reach 1000 at noon. Bob has 8 hours to claim the BTC before Alice can.
Where this is a bit wonky is that the result is metastable. Let’s assume that neither Alice nor Bob is right: The deficit is 50 blocks short.
At noon, Bob cannot claim. But in 8 hours he can! But also in 8 hours Alice can claim too.
So who wins?
The answer is either! Using a POWSWAP you either want to be really right or really wrong.
We’ll see some cool results around why this might not be a huge deal later.
Let’s flip the powswap around now, for a surplus of blocks. Bob thinks the blocks will be 1000, Alice thinks 1100.
Under this model if Alice is right there should be that many blocks by that time, and if Bob is right there should not be and a resaonable amount of time later Bob can claim.
It’s a bit harder to see, but we can even implement this logic more simply as just:
Then, if a week goes by first without seeing 1100 blocks, Bob can claim. If the 1100 blocks show well before the week is up, then Alice can claim. If neither are really right then it’s metastable and either could win.
There are a myriad of different combinations of locktimes and heights that you can use to do this correctly, we won’t focus too much on that in this post, and we’ll let our contract users decide what parameters they want. Let the analysts figure out what the right combo of locktimes and stuff is to hedge different risks. They should get paid for something, right?
One of the wrinkles is that the less time you have in your contract, the more metastable it is. The more time you have, expecially across difficulty adjustments, the more the deficits can be erased.
In the example I gave above, it is not! However, if you have CTV then one party can unilaterally open a hashrate derivative for other parties, and that matters quite a lot!
This means that when we do implement it, we will use then
because if you
want the pre-signature version you can use CTV Emulators.
First we’ll start by writing some code to be able to describe the locktimes under which some outcome is considered “resolved”. We’ll write a container type (the data we actually need) and then we’ll write a verifier type that makes for a convenient API for human input. It’s kind of gross, so you can skip the verifier type code and just imagine you put in the correct parameters.
/// `ContractVariant` ensures that we either set a Relative Height and Absolute
/// Time or a Relative Time and Absolute Height, the two valid combinations, or
/// just one.
///
/// Note these are unlocking conditions for each participant.
///
/// Validity is ensured through smart constructor
#[derive(JsonSchema, Deserialize, Clone, Copy)]
#[serde(try_from = "ValidContractVariant")]
pub struct ContractVariant(Option<AnyRelTimeLock>, Option<AnyAbsTimeLock>);
/// In order to test for coherence here, we should convert
/// ValidContractVariant to ContractVariant.
///
/// The coherence rules should match one ruleset of:
/// - a single type of TimeLock (Relative Height, Relative Time, Absolute Time,
/// Absolute Height)
/// - a mixed TimeLock of just Relative Height/Absolute Time or just Relative
/// Time/Absolute Height
#[derive(JsonSchema, Deserialize, Clone)]
struct ValidContractVariant(Vec<AnyTimeLock>);
impl TryFrom<ValidContractVariant> for ContractVariant {
type Error = CompilationError;
fn try_from(vcv: ValidContractVariant) -> Result<Self, Self::Error> {
let abs: Vec<_> = vcv
.0
.iter()
.filter_map(|v| {
if let AnyTimeLock::A(a) = v {
Some(a)
} else {
None
}
})
.collect();
let rel: Vec<_> = vcv
.0
.iter()
.filter_map(|v| {
if let AnyTimeLock::R(r) = v {
Some(r)
} else {
None
}
})
.collect();
let all_rh = rel.iter().all(|v| matches!(v, AnyRelTimeLock::RH(c)));
let all_rt = rel.iter().all(|v| matches!(v, AnyRelTimeLock::RT(c)));
#[derive(Debug)]
struct LocalError(&'static str);
impl std::fmt::Display for LocalError {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::result::Result<(), std::fmt::Error> {
self.0.fmt(f)
}
}
impl std::error::Error for LocalError {}
if !(all_rh || all_rt) {
return Err(CompilationError::custom(LocalError(
"Must have some timelock set!",
)));
}
let all_ah = abs.iter().all(|v| matches!(v, AnyAbsTimeLock::AH(c)));
let all_at = abs.iter().all(|v| matches!(v, AnyAbsTimeLock::AT(c)));
if !(all_ah || all_at) {
return Err(CompilationError::custom(LocalError(
"Incoherent Absolute Timelocks (mixed height/time)",
)));
}
let relative = rel.iter().max_by_key(|v| AnyRelTimeLock::get(v)).cloned();
let absolute = abs.iter().max_by_key(|v| AnyAbsTimeLock::get(v)).cloned();
if matches!((relative, absolute), (None, None)) {
return Err(CompilationError::custom(LocalError(
"Must have some timelock set!",
)));
}
if (all_rt && all_at) || (all_rh && all_rt) {
return Err(CompilationError::custom(LocalError(
"Must mix {Relative,Absolute} Height and Absolute time!",
)));
}
Ok(ContractVariant(relative.cloned(), absolute.cloned()))
}
}
impl ContractVariant {
fn get_relative(&self) -> AnyRelTimeLock {
self.0.unwrap_or(RelTime::from(0).into())
}
fn get_abs(&self) -> AnyAbsTimeLock {
self.1.unwrap_or(AbsHeight::try_from(0).unwrap().into())
}
}
With that out of the way, let’s now define our contract data:
/// Instructions for a Payment from an outcome
#[derive(JsonSchema, Deserialize, Clone)]
pub struct Pays {
sats: AmountU64,
to: PublicKey,
}
/// A `Outcome` is a contract where
#[derive(JsonSchema, Deserialize, Clone)]
pub struct Outcome {
/// # Variant
/// if the base is time or height for the relative leg.
unlocks_if: ContractVariant,
/// # Outcome
/// Payments to make (should be >= 1)
outcome: Vec<Pays>,
}
/// A `PowSwap` is a contract where
#[derive(JsonSchema, Deserialize, Clone)]
pub struct PowSwap {
/// # Parties
pub outcomes: [Outcome; 2],
/// # Cooperate Key
coop: Vec<PublicKey>,
}
impl Contract for PowSwap {
declare! {then, Self::payoff}
declare! {finish, Self::cooperate}
}
As you can see, it’s pretty simple. We just need a set of keys to ‘opt out’ of the on-chain execution, and a set of outcomes and their unlocking conditions. We can pay an arbitrary number of parties.
Now to finish, let’s implement the logic. It’s really simple, we just create the (2) transactions and assign the sequences/locktimes properly.
impl PowSwap {
#[guard]
fn cooperate(self, ctx: Context) {
Clause::And(self.coop.iter().cloned().map(Clause::Key).collect())
}
fn make_payoffs(&self, ctx: Context, payments: &[Pays]) -> Result<Builder, CompilationError> {
let mut bld = ctx.template();
for Pays { sats, to } in payments {
bld = bld.add_output(sats.clone().into(), to, None)?;
}
Ok(bld)
}
#[then]
fn payoff(self, mut base_ctx: Context) {
let mut ret: Vec<Result<Template, _>> = vec![];
for (i, path) in self.outcomes.iter().enumerate() {
let ctx = base_ctx.derive_num(i as u64)?;
let v = self
.make_payoffs(ctx, &path.outcome)?
.set_sequence(-1, path.unlocks_if.get_relative())?
.set_lock_time(path.unlocks_if.get_abs())?
.into();
ret.push(Ok(v));
}
Ok(Box::new(ret.into_iter()))
}
}
That wasn’t so bad now, was it?
We already said we’re not going to analyze the profit of these contracts, but I want to give a couple cool ways to use these.
One thing that I think would be important to settling a hashrate derivative would be to set it for, say, 6 months forecast and then try to roll the strategy at 3 months cooperatively.
This way you don’t have trouble with metastability as you and your counterparty can update forecasts and re-enter the contract, or go separate ways.
Well what if instead of settling on-chain, you nested these in LN channels? And then every microsecond you don’t see a block being advertised and broadcast, you update your probabilities and try to adjust with your counterparty. It becomes pretty neat becuase you essentially make a hashrate perpetual where if your counterparty dies then you settle on-chain (if they’re really dead, you just win), but you can update your forecasts on whatever frequency you want. All trustlessly.
This opens the door for HFT-ing information about the rate of block production. Knowing a block is mined and getting it relayed to you before your counterparty gives you an edge in trading.
Maybe this pays for really really good block relaying infrastructure?
Hey, it’s me. Your old friend Decentralized Coordination Free Mining Pools. What if we made – using CTV – the channels/payouts by default resolve into some sort of hashrate future, and we had an automated hedging market maker that could incorporate your desired side of a trade from old hash shares into opening new positions for you every block. If it was in channels you could immediately turn these into hashrate perps.
If you’re a miner and you mine, say, 2 blocks a day, then you can usually expect to be able to settle your own metastable hashrate derivatives as long as the metastable window isn’t smaller than ~12 hours. This means that while normie pleb users might struggle with closing their derivatives, miner-to-miner hashrate derivatives should be actually pretty safe if you stay in your bounds.
This idea composes beautifully with the options we saw yesterday. What if I want the option for the next week to open up a 6 month hashrate contract with you?
Just toss it into an Expiring Under Funded Option contract and you got it. And because we represented these as Dutch Auctionable NFTs, you can advertise the position you’re willing to open to the network and take the best offer for this option.
Sapio composes. Legit forreal.
DeFi is coming to Bitcoin.
And it’s going to help with securing the base layer of Bitcoin by permitting trustless financialization of investments in hashrate.
Have a great day. P.s. now is a good time to join utxos.org/signals if you think CTV is a great next step in Bitcoin Development’s journey.