This post is syndicated from rubin.io.
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
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!
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…
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.
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…).
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.
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).
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 one1.
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).
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.
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.
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.
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.
Note this is a case where unrolling can be used, but the contract sizes can blow up kinda quick, so careful programming might be needed or you might need to say that it can only be claimed that Jimmy is dead once or twice before he just gets all the money. Recursive covenants would not nescessarily have this issue. ↩︎