Here you'll find an assorted mix of content from yours truly. I post about a lot
of things, but primarily
Day 11: Rubin's Bitcoin Advent Calendar
08 Dec 2021
Welcome to day 11 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
You are going to die.
Merry Christmas! Hopefully not any time soon, but one of these days you will shuffle off this mortal coil.
When that day comes, how will you give your loved ones your hard earned bitcoin?
You do have a plan, right?
This post is a continuation of the last post on Vaults. Whereas Vaults focus on trying to keep your coins away from someone, Inheritance focuses on making sure someone does get your coins. Basically opposites!
Basic Bitcoin Plans
Let’s say you’re a smarty pants and you set the following system up:
(2-of-3 Multisig of my keys) OR (After 1 year, 3-of-5 Multisig of my 4 family members keys and 1 lawyer to tie break)
Under this setup, you can spend your funds secured by a multisig. You have to
spend them once a year to keep your greedy family away, but that’s OK.
Until one day, you perish in a boating accident (shouldn’t have gone to that Flamin’ Hot Cheetos Yach Party in Miami).
A year goes by, no one knows where your 2-of-3 keys are, and so the family’s
backup keys go online.
They raid your files and find a utxoset backup with descriptors and know how to
combine their keys (that you made for them most likely…) with offline signing
devices to sign a PSBT, and the money comes out.
If the family can’t agree, a Lawyer who has your will can tie break the execution.
Except wait…
Your kids are assholes, just like your spouse
So your piece of shit husband/wife doesn’t think the kids should get anything (RIP
college fund), so count them out on signing the tuition payments.
Now we’re down to your 3 kids agreeing and your 1 lawyer.
Your Lawyer thinks your spouse has a bit of a case, so the whole things in
probate as far as they are concerned.
And the kids? Well, the kids don’t want to go to college. You just gifted them
42069 sats each, enough to pay for a ticket on Elon Musk’s spaceship. So they
get together one night, withdraw all the money, and go to Mars. Or the Casino.
Little Jimmy has never seen so much money, so he goes to Vegas for a last huzzah
before the Mars trip, but he blows it all. So Jimmy stays behind, satless, and
the other kids go to mars.
Well That Sucked
And it didn’t have to! What if you could express your last will and testament in
Bitcoin transactions instead of in messy messy multisigs. You Can! Today! No new
features required (although they’d sure be nice…).
Building Inheritence Schemes with Sapio
You can make inheritence schemes with Sapio! While it does benefit from having
CTV enabled for various reasons, technically it can work decently without CTV by
pre-signing transactions with a CTV emulator.
Here we’ll develop some interesting primitives that can be used to make various
inheritence guarantees.
Making a better Dead Man Switch
First off, let’s make a better dead man switch. Recall we had to move our funds once a year because of the timelocks.
That was dumb.
Instead, let’s make a challenge of liveness! (again, deep apologies on these
examples, I’m a bit behind on the series so haven’t checked as closely as I
would usually…)
/// Opening state of a DeadManSwitch
#[derive(Clone)]
struct Alive {
/// Key needed to claim I'm dead
is_dead: bitcoin::PublicKey,
/// If someone says i'm dead but I'm alive, backup wallet address
is_live: bitcoin::Address,
/// My normal spending key (note: could be a Clause instead...)
key: bitcoin::PublicKey,
/// How long you have to claim you're not dead
timeout: RelTime,
/// Addresses for CPFP Anchor Outputs
is_dead_cpfp: bitcoin::Address,
is_live_cpfp: bitcoin::Address,
}
impl Alive {
#[guard]
fn is_dead_sig(self, ctx: Context) {
Clause::Key(self.is_dead.clone())
}
/// only allow the is_dead key to transition to a CheckIfDead
#[then(guarded_by="[Self::is_dead_sig]")]
fn am_i_dead(self, ctx: Context) {
let dust = Amount::from_sat(600);
let amt = ctx.funds();
ctx.template()
// Send all but some dust to CheckIfDead
.add_output(amt - dust, &CheckIfDead(self.clone()), None)?
// used for CPFP
.add_output(
dust,
&Compiled::from_address(self.is_dead_cpfp.clone(), None),
None,
)?
.into()
}
/// Allow spending like normal
#[guard]
fn spend(self, ctx: Context) {
Clause::Key(self.key.clone())
}
}
impl Contract for Alive {
declare! {finish, Self::spend}
declare! {then, Self::am_i_dead}
}
/// All the info we need is in Alive struct already...
struct CheckIfDead(Alive);
impl CheckIfDead {
/// we're dead after the timeout and is_dead key signs to take the money
#[guard]
fn is_dead(self, ctx: Context) {
Clause::And(vec![Clause::Key(self.0.is_dead.clone()), self.0.timeout.clone().into()])
}
/// signature required for liveness claim
#[guard]
fn alive_auth(self, ctx: Context) {
Clause::Key(self.key.clone())
}
/// um excuse me i'm actually alive
#[then(guarded_by="[Self::alive_auth]")]
fn im_alive(self, ctx: Context) {
let dust = Amount::from_sat(600);
let amt = ctx.funds();
ctx.template()
/// Send funds to the backup address!
.add_output(
amt - dust,
&Compiled::from_address(self.0.is_live.clone(), None),
None,
)?
/// Dust for CPFP-ing
.add_output(
dust,
&Compiled::from_address(self.0.is_live_cpfp.clone(), None),
None,
)?
.into()
}
}
impl Contract for CheckIfDead {
declare! {finish, Self::is_dead}
declare! {then, Self::im_alive}
}
In this example, the funds start in a state of Alive, until a challenger calls
Alive::am_i_dead
or the original owner spends the coin. After the call of
Alive::am_i_dead
, the contract transitions to CheckIfDead state. From this state,
the owner has timeout
(either time or blocks) time to move the coin to their
key, or else the claimer of the death can spend using CheckIfDead::is_dead
.
Of course, we can clean up this contract in various ways (e.g., making the
destination if dead generic). That could look something like this:
struct Alive {
is_dead_cpfp: bitcoin::Address,
is_live_cpfp: bitcoin::Address,
// note that this permits composing Alive with some arbitrary function
is_dead: &dyn Fn(ctx: Context, cpfp: bitcoin::Address) -> TxTmplIt,
is_live: bitcoin::Address,
key: bitcoin::PublicKey,
timeout: RelTime,
}
impl CheckIfDead {
#[then]
fn is_dead(self, ctx: Context) {
self.0.is_dead(ctx, self.0.is_dead_cpfp.clone())
}
}
This kind of dead man switch is much more reliable than having slowly eroding
timelocks since it doesn’t require regular transaction refreshing, which was the
source of a bug in Blockstream’s federation
code.
It also requires an explicit action to claim a lack of liveness, which also
gives information about the trustworthiness of your kids (or any exploits of
their signers).
Not so fast
What if we want to make sure that little Jimmy and his gambling addiction don’t
blow it all at once… Maybe if instead of giving Jimmy one big lump sum, we
could give a little bit every month. Then maybe he’d be better off! This is
basically an Annuity contract.
Now let’s have a look at an annuity contract.
struct Annuity {
to: bitcoin::PublicKey,
amount: bitcoin::Amount,
period: AnyRelTime
}
const MIN_PAYOUT: bitcoin::Amount = bitcoin::Amount::from_sat(10000);
impl Annuity {
#[then]
fn claim(self, ctx:Context) {
let amt = ctx.funds();
// Basically, while there are funds left this contract recurses to itself,
// until there's only a little bit left over.
// No need for CPFP since we can spend from the `to` output for CPFP.
if amt - self.amount > MIN_PAYOUT {
ctx.template()
.add_output(self.amount, &self.to, None)?
.add_output(amt - self.amount, &self, None)?
.set_sequence(-1, self.period.into())?
.into()
} else if amt > 0 {
ctx.template()
.add_output(amt, &self.to, None)?
.set_sequence(-1, self.period.into())?
.into()
} else {
// nothing left to claim
empty()
}
}
}
We could instead “transpose” an annuity into a non-serialized form. This would
basically be a big transaction that has N outputs with locktimes on claiming
each. However this has a few drawbacks:
-
Claims are non-serialized, which means that relative timelocks can only last
at most 2 years. Therefore only absolute timelocks may be used.
-
You might want to make it possible for another entity to counterclaim Jimmy’s
funds back, perhaps if he also died (talk about bad luck). In the transposed version, you would need to make N proof-of-life challenges v.s. just one.
-
You would have to pay more fees all at once (although less fees overall if feerates increase or stay flat).
-
It’s less extensible – for example, it would be possible to do a lot of cool
things with serialization of payouts (e.g., allowing oracles to inflation adjust
payout rate).
Splits
Remember our annoying spouse, bad lawyer, etc? Well, instead of giving them a multisig, imagine
we use the split function as the end output from our CheckIfDead:
fn split(ctx: Context, cpfp: bitcoin::Address) -> TxTmplIt {
let dust = Amount::from_sat(600);
let amt = ctx.funds() - dust;
let mut ctx.template()
.add_output(dust, &Compiled::from_address(cpfp, None), None)?
.add_output(amt*0.5, &from_somewhere::spouse_annuity, None)?
.add_output(amt * 0.1666, &from_somewhere::kids_annuity[0], None)?
.add_output(amt*0.1666, &from_somewhere::kids_annuity[1], None)?
.add_output(amt*0.1666, &from_somewhere::kids_annuity[2], None)?
.into()
}
This way we don’t rely on any pesky disagreement over what to sign, the funds
are split exactly how we like.
Oracles and Lawyers
Lastly, it is possible to bake into these contracts all sorts of
conditionallity.
For example, imagine an Annuity that only makes payouts if a University
Attendance Validator signs your tuition payment, otherwise you get the coins on
your 25th Birthday.
struct Tuition {
/// keep this key secret from the school
to: bitcoin::PublicKey,
enrolled: bitcoin::PublicKey,
school: bitcoin::PublicKey,
amount: bitcoin::Amount,
period: AnyRelTime,
birthday: AbsTime,
}
const MIN_PAYOUT: bitcoin::Amount = bitcoin::Amount::from_sat(10000);
impl Tuition {
#[guard]
fn enrolled(self, ctx: Context) {
Clause::And(vec![Clause::Key(self.enrolled), Clause::Key(self.to)])
}
#[then(guarded_by="[Self::enrolled]")]
fn claim(self, ctx:Context) {
let amt = ctx.funds();
if amt - self.amount > MIN_PAYOUT {
// send money to school
ctx.template()
.add_output(self.amount, &self.enrolled, None)?
.add_output(amt - self.amount, &self, None)?
.set_sequence(-1, self.period.into())?
.into()
} else if amt > 0 {
// give the change to child
ctx.template()
.add_output(amt, &self.to, None)?
.set_sequence(-1, self.period.into())?
.into()
} else {
empty()
}
}
#[guard]
fn spend(self, ctx: Context) {
Clause::And(vec![self.birthday.into(), Clause::Key(self.to)])
}
}
The oracle can’t really steal funds here – they can only sign the already
agreed on txn and get the tuition payment to the “school” network. And on the
specified Birthday, if not used for tuition, the funds go to the child directly.
Where do these live?
In theory what you’d end up doing is attaching these to every coin in you wallet
under a dead-man switch.
Ideally, you’d put enough under your main “structured” splits that you’re not
moving all to often and then you would have the rest go into less structured
stuff. E.g., the college fund coins you might touch less frequently than the
coins for general annuity. You can also sequence some things using absolute
timelocks, for example.
In an ideal world you would have a wallet agent that is aware of all your UTXOs
and your will and testament state and makes sure to regenerate the correct
conditions whenever you spend and then store them durably, but that’s a bit
futuristic for the time being. With CTV the story is a bit better, as for many
designs you could distribute a WASM bundle for your wallet to your family and
they could use that to generate all the transactions given an output, without
needing to have every presigned transaction saved.
This does demonstrate a relative strength for the account model, it’s much
easier to keep all your funds in once account and write globally correct
inheritence vault logic around it for all your funds, computed across
percentages. No matter the UTXO model covenant, that someone might have multiple
UTXOs poses an inherent challenge in doing this kind of stuff properly.
What else?
Well, this is just a small sampling of things you could do. Part of the power of
Sapio is that I hope you’re feeling inspired to make your own bespoke
inhertience scheme in it! No one size fits all, ever, but perhaps with the power
of Sapio available to the world we’ll see a lot more experimentation with what’s
possible.
Till next time – Jeremy.
Day 10: Rubin's Bitcoin Advent Calendar
07 Dec 2021
Welcome to day 10 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 “Vault” is a general concept for a way of protecting Bitcoin from theft
through a cold-storage smart contract. While there is not formal definition of
what is and is not a Vault, generally a Vault has more structure around a
withdrawal than just a multisig.
One of the earlier
references
for Vaults was a design whereby every time you request to withdraw from it you
can “reset” the request within a time limit. This means that while an attacker
might steal your keys, you can “fight” to make it a negative sum game – e.g.,
they’ll just keep on paying fees to eventually steal an amount less than they
paid. This might serve to disincentivize hacking exchanges if hackers are less
likely to actually get coins.
Similar Vaults can be built using Sapio, but the logic for them involves
unrolling the contract a predefined number of steps. This isn’t bad because if
the period of timeout is 1 week then just unrolling 5,200 times gets you one
thousand years of hacking disincentive.
The contract for that might look something like this in Sapio (note: I was
running behind on this post so I may make modifications to make these examples
better later):
struct VaultOne {
/// Key that will authorize:
/// 1) Recursing with the vault
/// 2) Spending from the vault after not moved for a period
key: bitcoin::PublicKey,
/// How long should the vault live for
steps: u32,
}
impl VaultOne {
/// Checks if steps are remaining
#[compile_if]
fn not_out_of_steps(self, ctx: Context) {
if self.steps == 0 {
ConditionalCompileType::Never
} else {
ConditionalCompileType::NoConstraint
}
}
#[guard]
fn authorize(self, ctx: Context) {
Clause::Key(self.key.clone())
}
/// Recurses the vault if authorized
#[then(compile_if = "[Self::not_out_of_steps]", guarded_by = "[Self::authorize]")]
fn step(self, ctx: Context) {
let next = VaultOne {
key: self.key.clone(),
steps: self.steps - 1,
};
let amt = ctx.funds();
ctx.template()
.add_output(amt, &next, None)?
// For Paying fees via CPFP. Note that we should totally definitely
// get rid of the dust limit for contracts like this, or enable
// IUTXOS with 0 Value
.add_output(Amount::from_sat(0), &self.key, None)?
.into()
}
/// Allow spending after a week long delay
#[guard]
fn finish(self, ctx: Context) {
Clause::And(vec![
Clause::Key(self.key.clone()),
RelTime::try_from(Duration::from_secs(7 * 24 * 60 * 60))
.unwrap()
.into(),
])
}
}
/// Binds the logic to the Contract
impl Contract for VaultOne {
declare! {then, Self::step}
declare! {finish, Self::finish}
}
But we can also build much more sophisticated Vaults that do more. Suppose we
want to have a vault where once a week you can claim a trickle of bitcoin into a
hot wallet, or you can send it back to a cold storage key. This is a “structured
liquidity vault” that gives you time-release Bitcoin. Let’s check out some code
and talk about it more:
#[derive(Clone)]
struct VaultTwo {
/// Key just for authorizing steps
authorize_key: bitcoin::PublicKey,
amount_per_step: bitcoin::Amount,
/// Hot wallet key
hot_key: bitcoin::PublicKey,
/// Cold wallet key
cold_key: bitcoin::PublicKey,
steps: u32,
}
impl VaultTwo {
#[compile_if]
fn not_out_of_steps(self, ctx: Context) {
if self.steps == 0 {
ConditionalCompileType::Never
} else {
ConditionalCompileType::NoConstraint
}
}
#[guard]
fn authorized(self, ctx: Context) {
Clause::Key(self.authorize_key.clone())
}
#[then(compile_if = "[Self::not_out_of_steps]", guarded_by = "[Self::authorized]")]
fn step(self, ctx: Context) {
// Creates a recursive vault with one fewer steps
let next = VaultTwo {
steps: self.steps - 1,
..self.clone()
};
let amt = ctx.funds();
ctx.template()
// send to the new vault
.add_output(amt - self.amount_per_step, &next, None)?
// withdraw some to hot storage
.add_output(self.amount_per_step, &self.hot_key, None)?
// For Paying fees via CPFP. Note that we should totally definitely
// get rid of the dust limit for contracts like this, or enable
// IUTXOS with 0 Value
.add_output(Amount::from_sat(0), &self.authorize_key, None)?
// restrict that we have to wait a week
.set_sequence(
-1,
RelTime::try_from(Duration::from_secs(7 * 24 * 60 * 60))?.into(),
)?
.into()
}
/// allow sending the remaining funds into cold storage
#[then(compile_if = "[Self::not_out_of_steps]", guarded_by = "[Self::authorized]")]
fn terminate(self, ctx: Context) {
ctx.template()
// send the remaining funds to cold storage
.add_output(self.amount_per_step*self.steps, &self.cold_key, None)?
// For Paying fees via CPFP. Note that we should totally definitely
// get rid of the dust limit for contracts like this, or enable
// IUTXOS with 0 Value
.add_output(Amount::from_sat(0), &self.authorize_key, None)?
.into()
}
}
impl Contract for VaultTwo {
declare! {then, Self::step, Self::terminate}
}
This type of Vault is particularly interesting for e.g., withdrawing from an
exchange business. Imagine a user, Elsa who wants to have a great cold storage
system. So Elsa sets up a xpub key and puts it on ice. She then generates a new
address, and requests that the exchange let the funds go to it. Later that
month, Elsa wants to buy a coffee with her Bitcoin so she has to thaw out her
cold storage to spend (maybe using a offline PSBT signing), and transfer the
funds to her destination or to a hot wallet if she wants a bit of extra pocket
money. Instead suppose Elsa sets up a timerelease vault. Then, she can set up
her cold vault and automatically be able to claim 1 Bitcoin a month out of it,
or if she notices some coins missing from her hot wallet redirect the funds
solely under her ice castle.
This has many benefits for an average user. One is that you can invest in your
cold storage of keys once in your life and only have to access it in
unexpected circumstance. This means that: users might elect to use something
more secure/inconvenient to access (e.g. strongly geo-sharded); that they won’t
reveal access patterns by visiting their key storage facility; and that they
don’t need to expose themselves to recurring fat-finger risk.
Getting a little more advanced
What are some other things we might want to do in a vault? Let’s do a quickfire
– we won’t code these here, but you’ll see examples of these techniques in
posts to come:
Send a percentage, not a fixed amount
Let the contract know the intended amount, and then compute the withdrawals as
percentages in the program.
Non-Key Destinations
In the examples above, we use keys for hot wallet, cold wallet, and authorizations.
However, we could very well use other programs! For example, imagine a
time-release vault that goes into a anti-theft locker.
Change Hot Wallet Every Step
This one is pretty simple – if you have N steps just provide a list of N
different destinations and use the i-th one as you go!
Topping up:
There are advanced techniques that can be used to allow depositing into a
vault after it has been created (i.e., topping up), but that’s too advanced to
go into detail today. For those inclined, a small hint: make the “top up” vault
consume an output from the previous vault, CTV commits to the script so you can
use a salted P2SH out.
Even more advanced
What if we want to ensure that after a withdraw funds are re-inserted into the Vault?
We’ll ditch the recursion (for now), and just look at some basic logic. Imagine
a coin is held by a cold storage key, and we want to use Sapio to generate a transaction
that withdraws funds to an address and sends the rest back into cold storage.
struct VaultThree {
key: bitcoin::PublicKey,
}
/// Special struct for passing arguments to a created contract
enum Withdrawal {
Send {
addr: bitcoin::Address,
amount: bitcoin::Amount,
fees: bitcoin::Amount,
},
Nothing,
}
/// required...
impl Default for Withdrawal {
fn default() -> Self {
Withdrawal::Nothing
}
}
impl StatefulArgumentsTrait for Withdrawal {}
/// helper for rust type system issue
fn default_coerce(
k: <VaultThree as Contract>::StatefulArguments,
) -> Result<Withdrawal, CompilationError> {
Ok(k)
}
impl VaultThree {
#[guard]
fn signed(self, ctx: Context) {
Clause::Key(self.key.clone())
}
#[continuation(guarded_by = "[Self::signed]", coerce_args = "default_coerce")]
fn withdraw(self, ctx: Context, request: Withdrawal) {
if let Withdrawal::Send { amount, fees, addr } = request {
let amt = ctx.funds();
ctx.template()
// send the rest recursively to this contract
.add_output(amt - amount - fees, self, None)?
// process the withdrawal
.add_output(amount, &Compiled::from_address(addr, None), None)?
// mark fees as spent
.spend_amount(fees)?
.into()
} else {
empty()
}
}
}
impl Contract for VaultThree {
declare! {updatable<Withdrawal>, Self::withdraw}
}
Now we’ve seen how updatable continuation clauses can be used to dynamically
pass arguments to a Sapio contract and let the module figure out what the next
transactions should be, managing recursive and non-enumerated state transitions
(albeit with a trust model).
That’s probably enough for today, before I make your head explode. We’ll see more examples soon!
Day 9: Rubin's Bitcoin Advent Calendar
06 Dec 2021
Welcome to day 9 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
We’re through the basics sections of the Advent calendar ow! Time for some more…
specific content on the bleeding edge!
This post is your introduction to the world of
Sapio. Sapio is the programming framework I’ve
been developing for Bitcoin Smart Contracts. There’s a ton of material on the
website, so this post is going to be a bit
high-level and then you should jump into the docs after to learn more.
What the heck is Sapio?
Sapio is a tool that helps you design and use Bitcoin smart contracts based on
covenants (like CTV) as well as manage potentially recursive state transitions
at terminal states.
That’s a mouthful and a half… let’s break it down with a very basic vault
deposit example.
Suppose I have 10 bitcoin sitting in my normal wallet. I want to deposit it to
an exchange. I go to my exchange and request an address to deposit to. The
exchange wants their coins to be in a special cold storage whereby any move from
cold storage has to “mature” for 10 days since it was claimed before it’s
spendable as a hot-spend, otherwise it stays in cold. The hot wallet has logic
such that any unused funds after it transacts, goes back into the cold-storage
contract. We saw a contract like this in the day
7 post.
The exchange can use Sapio to generate an address that expects 10 coins and
encodes this cold-to-hot logic without requiring the cold keys be online! Better
than that, I don’t even have to contact the exchange for the address. The
exchange can distribute a codesigned Sapio WASM applet that runs on my own
machine locally. I download the applet into my Sapio Studio GUI and that
generates the exchange deposit UX form for the contract that I (or my wallet)
automatically fills out and then generates a proper address/spending
transaction.
Upon receipt of the deposit information, (which can in certain circumstances be
completely on-chain in the txn, so no need for a separate communication
channel), the exchange can us the WASM to generate an identical deposit program
to verify the user isn’t cheating somehow. Bada-bing-bada-boom!
We’ll see in close detail examples like this coming in the following posts, but
to sum up, Sapio helped us with the following:
- Authoring a Smart Contract Application for a cold storage deposit solution
- Distributing it as a deterministic executable with a GUI User using it to make a
deposit
- Receiving funds as a depositee directly into a smart contract
- Generating withdrawal transactions out of the vault
- Putting the remaining funds back into the cold storage
This is not a hypothetical, all of these components exist and are usable today!
Where there is an asterisk is that BIP-119 CTV does not yet exist, and so for
apps like this the exchange would have to run some kind of signing
server you connect to.
This works, but is a worse trust model. For some applications, you don’t need
CTV if you can get all of a contract’s parties to run their own oracles.
Therefore you can still accomplish a lot without a worse trust model with what’s
there today!
Over the remaining posts we’ll go into great detail on different applications
built in Sapio, but for now you can skim through
learn.sapio-lang.org to get started playing
around with your own designs.
Day 8: Rubin's Bitcoin Advent Calendar
05 Dec 2021
Welcome to day 8 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 this post we’ll rapid fire roll through a bunch of different smart contract
primitives, existing and proposed. For a more thorough reading, links will be
provided.
BIP-119 CTV CheckTemplateVerify
CTV is a general purpose smart contract opcode with full enumeration, no dynamic
state, no recursion, and primarily works through validation.
Essentially, CTV only lets you select a specific next transaction that can
occur. Consensus just checks a transaction hash against a CTV hash.
Although this seems to be limited functionality, it can be used with a template
metaprogramming system such as Sapio to create
sophisticated programs.
The limited functionality isn’t a bug, it’s a feature. CTV was designed to be
quick and easy to garner technical consensus with the entire Bitcoin community
as a simple and safe covenant without some of the issues more sophisticated
covenant systems might have. However, since its launch there’s been more
interest developing for more flexible covenants, which may take much longer to
deploy and deliver meaningful benefits to users.
CTV is also designed to work well with other opcodes that might be added (such
as CSFS, OP_AMOUNT, and OP_CAT), so it does not become irrelevant should more
features be added, it simply gets better.
CTV is currently a decently reviewed BIP pending more support from the community
for inclusion (see social signals).
Disclosure: I’m the author/advocate of BIP-119.
For more:
- Optech
- utxos.org
- Templates, Eltoo, and Covenants, Oh My!
- Shinobi’s Covenant Concerns
BIP-118 APO AnyPrevout
AnyPrevout is a culmination of research for the Lightning Network (dating back
to the original whitepaper) for creating a type of “rebindable” bitcoin
transaction that dramatically simplifies the protocols for LN by getting rid of
a lot of the complexities around storing state and closing channels
unilaterally. AnyPrevout helps make Decker Channels possible (or, confusingly,
sometimes called Eltoo not to be confused with L2).
The basics of how Anyprevout works is that it changes what parts a signature
signs to exclude the specifics of the coin being spent. This has some drawbacks
in terms of changing current invariants true of signatures, but it is generally
safe.
APO can also be used to implement something similar to CTV, but there are
sufficient differences between the two (including with respect to efficiency)
such that the proposals aren’t competitive.
APO is currently a decently reviewed BIP pending more support from the community
for inclusion. The largest blocker for wider support is a concrete functional
prototype of LN with Decker Channels, which would drive surety that APO has
“product market fit”. Certain developers believe that additional proposals, like
SIGHASH_BUNDLE, would be required to make it fully functional.
- My BIP-118 Review
- The BIP
- Eltoo/Decker Channels
- Templates, Eltoo, and Covenants, Oh My!
TLUV TapLeafUpdateVerify
TLUV is a proposed general purpose smart contract opcode that is open ended, has
dynamic local state, recursive, and is somewhat computational.
Essentially, TLUV lets you modify a Taproot Output being spent by changing the
toplevel key and script paths being spent. TLUV only can read and affect a
single input/output pair; the other outputs are unaffected. The functionality of
TLUV is very “specific” to the implementation details of Taproot, as it must
correctly modify the data structures behind it. For Example, you could have a
Taproot output with 10 coins and a script like:
[{"amt": 10,
"key": "multi(A,B,C)",
"scripts": ["signed(A) with up to 2 coins",
"signed(B) with up to 5 coins",
"signed(C) with up to 3 coins"]
}
]
and TLUV would enable you to transition to the following outputs:
[{"amt": 9,
"key": "multi(A,B,C)",
"scripts": ["signed(A) with up to 1 coins",
"signed(B) with up to 5 coins",
"signed(C) with up to 3 coins"]
},
{"amt": 0.25,
"address": "someone paid by A"
},
{"amt": 0.75,
"address": "someone else paid by A"
}
]
or even a full exit:
[{"amt": 9,
"key": "multi(B,C)",
"scripts": ["signed(B) with up to 5 coins",
"signed(C) with up to 3 coins"]
},
{"amt": 0.25,
"address": "someone paid by A"
},
{"amt": 0.75,
"address": "someone else paid by A"
}
{"amt": 1,
"address": "A's key (exiting funds)"
}
]
There are some potential footguns around modifying the top level key, as it
needs to be a valid Taproot key after tweaking.
TLUV as designed requires some form of OP_AMOUNT to enable the
recursive shared UTXO shown above.
There is no current concrete proposal (e.g. BIP) for TLUV, it’s open ended
research presently.
- Optech
- Mailing List
- My Mailing List Response
CSFS CheckSigFromStack
CheckSigFromStack, or CheckDataSig (note for experts: usually shorthand for the
verification-only version as there’s little point to check that something wasn’t
signed by someone) is an opcode which checks an arbitrary message was signed by
a key. Normally, when a Bitcoin script checks a signature, the message must be
a hash of the current transaction computed in accordance with the requested
transaction hashing program.
CSFS has a couple “basic” applications that could be useful. For example, one
might write a program where either a key K signs a transaction normally, or it
signs a key which then signs a transaction. This allows the holder of a coin to
“delegate” the ownership of a coin to another key without moving the coin.
CSFS already exists in Bitcoin in some sense: using Lamport Signatures it is
currently possible to check a signature over 5 bytes of data. This is not
terribly useful, but one could imagine certain uses for e.g. delegating to the
specified signer the duration of a timelock.
CSFS really shines when it is combined with other opcodes. For example, CSFS
plus CTV can enable something similar to AnyPrevout and Eltoo. CSFS plus CAT
enables fully generic covenants in segwit V0, but not in Taproot (without some
sort of OP_TWEAK as well). This is best left to reading some additional
materials on the subject, but imagine if I first check the transaction signature
normally, and then I check it on the stack against the transaction itself pushed
onto the stack, which I used CAT to assemble from pieces. This would let me run
programmatic checks on all the components of a script).
While there is not currently a proposal for CSFS, it’s not terribly
controversial and the design would be relatively straightforward.
- BIP Suggestions
- Templates, Eltoo, and Covenants, Oh My!
- CSFS from Math (5 bytes)
OP_AMOUNT
OP_AMOUNT was proposed in 2017 by Johnson Lau (the earliest citation I could dig
up) through a scripting extension called PUSHTXDATA that allows arbitrary data
to be pushed on the stack. As a standalone extensions, getting the amount
spent/created on the stack (whether as a push opcode or an opcode with verify
semantics) would allow for smart contracts to either limit the amount being
spent or switch behavior based on the amount.
For example, with TLUV a Taproot branch can have an individual balance that can
be updated at the discretion of the branch holder. Suppose I had a script tree
that said Alice has 1 bitcoin and Bob has 20 Bitcoin. When Alice is spending,
the script would require that the corresponding output (e.g., input 0 output 0)
be reduced by at most 1 Bitcoin, and the output should be updated to change
Alice’s script to have 1-(spent amount) in the next instance.
As another example, CTV could be used with an OP_AMOUNT to enable a ultra high
security vault if the amount sent is greater than 1 Bitcoin and a lower security
vault if it is less than 1 Bitcoin.
There’s no current concrete proposal for OP_AMOUNT. Difficulties in adding it
remain because Bitcoin Scripts deal in 32-bit math and amounts are 64-bit values
(51 bits precisely).
- OP_PUSHTXDATA
- OP_IN_OUT_AMOUNT
SIGHASH_BUNDLE
Sighash Bundle is a part of an effort to make “Sighash Flags” more general.
Sighash Flags are a mini “programming language” to describe what parts of a
transaction a signer wants to sign for a transaction. Bundles in particular
allow a signer to select a range of inputs and outputs in a way that the bundle
description can be rebound to allow some form of post-hoc aggregation of
transactions.
It’s primarily proposed to help make Decker Channels work with a sub-protocol
called “layered commitments”. It’s possible for inclusion, but it has the same
issue as AnyPrevout, we need to see an end-to-end implementation of LN using it
to be sure that the technology is solving the problem it is designed to.
There’s no concrete implementation proposed yet.
- Mailing List Post
Transaction Sponsors is another proposal by yours truly.
The basic concept of Transaction Sponsors is to allow expressing logic that
Transaction B should only be in a block if Transaction A is also in the block.
In particular, the proposal says that a transaction with a 0 value output with
script OP_VER <txids>
would make the transaction valid only if the txids were
also in the block.
The ability to express such a dependency has implications for designing novel
smart contracts based on these dependencies, but this is not the focus of the
sponsors proposal with respect to mempool policy.
Instead, the Sponsors proposal is to use the ability to express additional
dependencies as a way of dynamically adding fees to transactions in the mempool
without relying on CPFP or RBF. This primitive is particularly helpful for
driving progress of smart contracts based on CTV or Decker Channels without
requiring any sort of transaction malleability.
There is currently an implementation and Draft BIP of Sponsors, but the BIP has
not been advanced for inclusion yet.
- Mailing List Post
- Post about difficulties of paying fees
OP_CAT (Or SHASTREAM)
OP_CAT is “deceptively simple”. All it enables is the ability to take an
argument “hello “ and an argument “world” and join them together into “hello
world”.
CAT was originally a part of Bitcoin, but it had some implementation flaws and
was removed by Satoshi in an emergency patch early on in Bitcoin’s history.
Although it is simple, it turns out that the ability to join bytestrings
together adds a remarkable variety of functionality to Bitcoin, including things
like quantum proof signatures and covenants. There are a couple different
variants of CAT that would be possible and have different tradeoffs, but largely
CAT and friends are not controversial in their design. What does make CAT
controversial is that because it has the propensity to introduce so many
surprising behaviors in Bitcoin, we might prefer to better understand the
impacts of users being able to author such advanced smart contracts.
- Quantum Proof Bitcoin
- Poelstra CAT Blog I
- Poelstra CAT Blog II
OP_TWEAK / ECMUL
These two opcodes enable manipulating an elliptic curve point on the stack for
use in a covenant or to compute a particular private key.
There’s no concrete proposal for this pair, but the implementations are
basically specified already by the requirements of the secp256k1 curve.
Adaptor Signatures
Adaptor Signatures are a technique that can be used with Schnorr signature and
do not require any additional forks to Bitcoin.
The basics of an Adaptor signature is that a party (or group of parties) can
create an object which either takes in a signature and reveals a secret or takes
a secret and reveals a signature.
These adaptors can be used in place of hash preimage locks for a variety of use
cases.
- Optech
Delegation / Graftroot
Delegation is a general concept whereby you can take a script and instead of
signing a transaction, you sign another script that can then execute. For
example, imagine if there is a coin that requires a signature of Alice and Bob
to spend. Suppose Alice wants to go offline, but Bob might want to transact.
Alice could sign a script requiring a signature from Carol that “substitutes”
for Alice’s signature in the future.
Delegation is currently possible in a somewhat roundabout way through
coin-delegation. This is where the other script fragment must be represented by a UTXO.
Graftroot is an extension to Taproot which would let the top-level key-path
signers sign delegating scripts, but not other tapscript branches. There are
also several confusingly named extensions and alternatives in the links below.
Delegation could also be combined with Anyprevout so that delegation
authorizations are bound to a specific coin or to a specific script. CSFS
enables a basic kind of delegation as well. This would enable, with Graftroot, a
version of Taproot where the trees are constructed interactively and do not have
any lookup cost.
Other than what’s presently possible, there are no concrete proposals for adding
new delegation features to Bitcoin.
- Coin Delegation
- Graftroot
- Entroot
- G’Root (not graftroot)
BIP-300 DriveChains
Drive chains are a highly application specific type of recursive covenant that
is designed to help sidechains operate by tracking sidechain deposits and
withdrawals with an on-chain miner driven voting system.
The sidechains would have the ability to run arbitrary smart contracts (at the
choice of the sidechain operators). Miners the upvote, downvote, or abstain from
voting on withdrawals through a special output type.
One of the main downsides to this approach is that the BIP-300 proposal as
written requires the addition of new global state databases, rather than local
state contained within the covenant transaction itself.
Overall Drivechains are relatively controversial among the community; with lots
of interest from the community and also some outspoken critics because of the
changes to Bitcoin’s incentive stability for consensus. It’s included here for
completeness and by request of what topics to cover in today’s post.
It’s the author’s opinion that while the concept of Drivechains is useful, the
implementation of it does not need to be as transactions inside of the existing block
space and instead could be tracked via a separate commitment (like Segwit). This could
happen if Drivechains were implemented via a more generliazed covenant rather than
application specific.
- BIP-300
- Drivechains
Elements Opcodes
Elements is Blockstream’s Bitcoin fork for their Liquid Sidechain. Elements has
planned to add a broad variety of opcodes that can help to accomplish a variety
of tasks, including many of the above, in addition to their existing extensions.
- Existing Opcodes
- Upgrade for Taproot
Breathe! That was a lot! There’s still other stuff that’s floating around, but
these are the top-of-mind primitives in my head for bringing more
programmability to Bitcoin.
Future posts will zero in on what’s possible with BIP-119 and Sapio and help
make the case that it is a fantastic next step in Bitcoin’s Upgrade journey by
showing (not telling) how one little limited opcode opens up an entire world of
possibilities, as well as laying out a – dare I say – personal roadmap for the
inclusion and development of other upgrades as a coherent narrative for Bitcoin.
Day 7: Rubin's Bitcoin Advent Calendar
04 Dec 2021
Welcome to day 7 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 this post we’ll review a concepts for thinking about different types of smart
contract capabilities and the implications of their availability.
Recursive v.s. Non Recursive
Recursive is pretty much just a fancy way of saying “loops”. This is sometimes
also called “Turing Complete”. That’s an even fancier way of saying loops. For
example, imagine a bitcoin contract with the following logic:
When Alice requests moving 1 coin to Bob by broadcasting a transaction with the
request, Alice has 24 hours to completely cancel the transfer by broadcasting
another transaction.
This is a looping contract because after cancelling Alice can immediately
re-request the transfer. An example of non-looping but similar logic would be:
When Alice requests moving 1 coin to Bob, Alice has 24 hours to cancel the
transfer by sending the coins to Alice’s backup key.
Here, the contract terminates after one canceled request by moving the coin
elsewhere. It’s possible to emulate recursive behavior a limited amount by
“unrolling” a loop. For example:
When Alice requests moving 1 coin to Bob, Alice has 24 hours to cancel the
transfer by sending the coins to (when Alice requests moving 1 coin to Bob,
Alice has 24 hours to cancel the transfer by sending the coins to Alice’s backup
key).
Here we substituted the backup key with a copy of the original logic. Now Alice
can make 2 cancellable requests before sending the money to the backup. This
looks recursive, and it can be expressed by a recursive meta-program. Meta
program is just a fancy term for a program that makes programs. But when we put
the contract into writing (e.g., an address on the chain), it has to be unrolled
for the specific number of iterations we want possible.
Unrolling is a very useful technique, and can be used in a broad variety of
circumstances. For example, imagine we unroll a contract a million times and
specify that transactions can only happen every 10 blocks. That covers like 200
years of contract execution. However, unrolling has it’s limits. When choices
(action A or B) are introduced, unrolling can be less effective since you have
and exponential blowup (that means unrolling even like 32 steps might be too
many). However, there are some tricks that can be employed by a clever and
careful programmer to reduce this complexity through, for example, memoization.
Fully Enumerated v.s. Open Ended
Suppose I have a contract which is supposed to strike an American option and
transfer a token. It might look like this:
If Alice is paid 1 BTC by December 25th, 2021 Midnight, then transfer 100 tokens
to Bob’s Control.
A fully enumerated contract would be expressed as:
If Alice is paid 1 BTC by December 25th, 2021 Midnight, then transfer 100 tokens
to Bob’s Address B.
Whereas an Open Ended contract would be expressed as:
If Alice is paid 1 BTC by December 25th, 2021 Midnight, then transfer 100 tokens
to the address Bob requested with the payment.
The key difference being that in the fully enumerated case we must know the
exact specifics of the contract and how it will execute, and in the open ended
contract case there are bits and pieces we can dynamically specify.
There are ways that a fully enumerated contract can emulate dynamic choice. For
example:
If Alice is paid 1 BTC by December 25th, 2021 Midnight, then transfer 100 tokens
to one of Bob’s Address B1, B2, or B3 at Bob’s discretion.
Now Bob can pick from one of three destinations in the future. However, these
options must have been known in advance (a priori). With an open ended contract,
the address could be generated after the fact (post hoc).
This is a separate concept from recursive or non recursive. A contract that
loops could loop through a set of fully enumerated states until reaching some
terminal predetermined “exit” state (e.g., a plain address). The option contract
described above is non-recursive, but can be open ended.
Client v.s. Consensus Validation
When you have a Bitcoin in an output, anyone who has run, say, Bitcoin Core can
tell that it is valid by seeing it in the UTXO set. But what happens if you want
to issue a token on top of Bitcoin with some set of unique rules? Bitcoin does
not know anything about these tokens, and so it would be possible to make an
invalid transaction (e.g., spending more value than you have). In order to
ensure the token is valid and not corrupt, one must trace every prior
transaction back to some “axiomatic” genesis transaction(s) minting the token.
These traces can be cached, but by default Bitcoin software will not enforce
that only valid transfers be made. We say that the token is Client validated
while the Bitcoin is Consensus validated.
Is one worse than the other? Not necessarily. While permitting invalid
transactions in the chain seems bad, as long as the invalid transactions can be
uniformly excluded by all who care about the token it is not much worse than the
work you do to run a Bitcoin full node anyways. There does seem to be some value
in the Bitcoin network preventing you from making invalid transactions, but the
network doesn’t stop you from making bad transactions (e.g., you could send
money to the wrong place).
Client side validation can check all sorts of properties, not just tokens. For
example, you could write a contract for an on-chain governed company and check
transactions for valid state transitions amending the rules.
The main drawback to client side validation comes when you want your contract to
interoperate with Bitcoin values. While client side validation can burn tokens
that are transferred invalidly, imagine an exchange contract that swaps Bitcoin
for Token. If the exchange contract sends more Bitcoin than it should, the
clients can tell that it was an invalid transaction but the Bitcoin is still
gone. Thus Client validated contracts are best left to things that don’t hold
Bitcoin. The exception to this rule is if the Client validated contracts admit a
custodian, a special monitor or set of monitors that handle the contracts
Bitcoin balances in e.g. a multisig. The monitors can client-side validate the
contracts and sign off on any balance transfers. The drawback to this approach
is trust, but in certain applications that we’ll see later the monitor could be
all of the participants themselves, which makes the application of the rules
trustless.
Validation v.s. Computation
Validation and Computation are two sides of the same coin. A simple example to
demonstrate:
Type |
Task |
Input |
Output |
Computation |
Sort the numbers [4,5,1] |
None |
[1,4,5] |
Validation |
Check that [4,5,1] is sorted by indexes A |
A = [2,0,1] |
True |
Validation is a computation, but hopefully it’s easier to perform the
validation computation than the computation itself.
In a Bitcoin transaction we are always validating that the transaction was
approved. A transaction in Bitcoin makes a clear precondition (the coins spent)
and postcondition (the coins sent). Even in cases where we have to do a lot of
computation to check the authorization, we still know the proposed outcome.
Compare to an Ethereum transaction: We pass some input to a function, and the
EVM verifies that our input was authorized (e.g., send 1 Eth to contract X with
message “hello”). Then, the side effects of that action are computed dynamically
by the EVM. For certain contracts, we might be able to predict what the side
effect will be (e.g., a 1:1 token exchange like Eth to Wrapped Eth), but for
other contracts (e.g., a floating exchange rate for Eth to Wrapped BTC) we will
get an outcome that can’t be fully predicted. It is possible for contracts to
choose to engineer themselves in a way to create more predictability, however in
Ethereum this does not result in an Invalid transaction, it results in a valid
transaction (that e.g. still costs gas) that has a result which is not taken.
For example, a transaction which says “Buy 1 WBTC for 15 ETH” might fail to
acquire WBTC since the price is now 16ETH, but the transaction would be valid
that you tried to make the trade and failed. This is because Ethereum’s base
layer is computational in nature with little validation: validation must be
built on top.
Sidenote: Malleability
For certain Bitcoin “covenant” transactions the validation/computation line can
be thin. Transactions must always be transactions in a block, but it’s possible
that in the future miners could receive “details” of a transaction and be
responsible for generating the appropriate transaction themselves. For example,
Blockstream released details on a noninteractive
feebumping
transaction, whereby a miner can dynamically compute a transaction that pays
them more fees the longer it takes to confirm.
In the case of malleability like this, it’s not as simple as saying “don’t do
it”, because miners have an incentive to extract the value if it is available.
Dynamic State
Contracts can have different types of state. State is just a fancy term for
information available to execution.
Global state is information that is observable from anywhere. An example of this
in Bitcoin is the UTXO Set: any transaction could spend any coin, and can “pull
it into scope” by naming it’s Outpoint (whether or not the transaction is valid
is another question). Another example of global state is the current block
height, used for validating things like lock times. In Ethereum, there is a much
expanded concept of Global state whereby contracts persist and allow read/write
access from other contracts, and unlike Bitcoin’s UTXO set, observing a piece of
information doesn’t destroy it like spending a coin does.
Local State is information observable only within your own context. For example,
a contract might hold balances for 3 different people, but the current values of
those split balances is not something queryable by outside parties. This also
includes implicit state, such as “the contract is currently pending an Action
from Alice” that are not explicitly coded.
Lastly, certain things are not State. An example of this is an authorizing
signature, which is ephemeral data that is used in the transaction execution but
does not have relevance for the continued execution of the contract and is not
particularly observable (which signature we use shouldn’t matter).
General v.s. Specific
A General contract primitive is something that can be used across many different
types of contract. A Specific contract implements well defined logic. In Bitcoin
and Ethereum, the focus is on General contract primitives that can be used many
ways. In some other ecosystems (e.g. NXT, Stellar), contract primitives have
much more specific functionality.
General/Specific ends up being more of a spectrum than a binary. Certain
contract primitives might be very specific but find general use, similarly some
general primitives might be more general than others.
For example, the Lightning Network on Bitcoin has pursued a path of using
general purpose extensions to Bitcoin so as not to “special case” payment
channels. But is that worth it? Might Payment Channels be cheaper, easier to
use, etc if we just designed built-in channels from the get-go? Perhaps yes, but
then it might be harder to embed other things or incorporate new innovations
into Lightning if it had to fit a single mold.
This isn’t an exhaustive list of topics by any means, but it should be a good
primer for thinking about upgrade proposals that people discuss in Bitcoin.
You’ll find out more about that in… tomorrow’s post!.