← index

Using OP_VAULT for recovery

An archive of delvingbitcoin.org · view original topic →

Antoine Poinsot · #1 ·

I was invited to give a talk about James’ OP_VAULT in Amsterdam. Since he’s done pretty extensive communication around the vault usecase already, and since i’ve been working on Liana for the past year, i decided to make the talk about (ab)using OP_VAULT for recovery. Figured a more in depth inquiry than permitted by a talk would be worth sharing with people on this platform.

What’s the value proposition of Liana?

Liana is a Bitcoin wallet which lets one specify timelocked spending path(s) to be used for recovery purposes.

Here is a list of usecases:

How can it be improved by leveraging OP_VAULT?

Liana is an existing wallet for today’s Bitcoin. As such it doesn’t use any form of covenant, just a simple OP_CSV. This means time starts ticking for the recovery path when you receive a coin. This in turn means user would have to use their wallet regularly or “refresh” their coins every so often if they don’t want (some of) the recovery path(s) to become available.

Not a deal breaker, but it certainly does add some friction to the UX. It seems it would be better to be able to trigger a “recovery process” and only then have the time start ticking. That’s what OP_VAULT permit. Roughly speaking just use an OP_VAULT in the script to receive coin with the “leaf script” (in OP_VAULT BIP terminology) being the recovery script.

Another improvement over today’s Liana is how the OP_VAULT recovery would presumably be triggered on all coins at the same time and the recovery path would become available at the same time for all coins to be recovered. That’s not the case in today’s Liana since not all coins were received at the same time.

It’s not a silver bullet, since you still need to check every so often whether a recovery wasn’t triggered. But you don’t need to make a transaction when doing so. That’s less burden, less cost and less potential privacy footgun.

Let’s go through the use cases listed above to see how we would implement them using OP_VAULT.

Safety net / “More secure” backups

This is the simplest case. 1 primary key (the owner of the coins), 1 recovery key (the backed up key or the third party key).

Today’s Taproot tree:

OP_VAULT Taproot tree:

Note the owner of the coins can always clawback any triggered recovery attempt (within 1y of it being triggered) since the internal key is retained.

Inheritance

This one’s still pretty simple, but it’s got a multisig and 2 different timelocks which raise interesting questions.

Today’s Taproot tree:

There is two ways this can be translated to using OP_VAULT. The first, simplest way would be to use 3 OP_VAULT branches like so:

However this has the downside that any party may continuously re-trigger the recovery, not letting any timelock expire. In addition, one timelock expiring does not lead to the following expiring tl2- tl1 blocks later. If the first timelock path is chosen, any attempt to use the second path needs to restart all over again.

This can be fixed by using a clumsier Taproot tree:

<spouse key> OP_CHECKSIG OP_SWAP <child key> OP_CHECKSIG OP_ADD OP_SWAP OP_IF
  0
OP_ELSE
  <144 * 365> OP_CHECKSEQUENCEVERIFY OP_0NOTEQUAL
OP_ENDIF
OP_ADD OP_SWAP OP_SIZE OP_0NOTEQUAL OP_IF
  <notary> OP_CHECKSIGVERIFY <144 * 455> OP_CHECKSEQUENCEVERIFY OP_0NOTEQUAL
OP_ENDIF
OP_ADD 3 OP_EQUAL

Now there is a single shot so you want any two keys from the set of 3 to be able to trigger a recovery. There is no way to keep restarting it, if the spouse and the child don’t get along it will automatically decay to including the notary as signatory.

In this specific example it’s probably fine to use the nicer version with multiple OP_VAULT leaves because there is no reason for any two parties to shoot themselves in the foot. It’s not always the case however.

Decaying multisig

Now let’s look at a decaying multisig for a 5-stakeholders organization. Start from a 3of5 multisig between all the stakeholders. In case 1 of them loses their key, after 1 year it becomes a 3of6 with a notary. After a year and 3 months, it becomes a 2of6 between these same persons.

The recovery script is then:

OP_IF
  <stk1> CHECKISG <stk2> CHECKISGADD <stk3> CHECKISGADD <stk4> CHECKISGADD <stk5> CHECKISGADD <notary> CHECKISGADD 3 OP_EQUALVERIFY <144 * 365> OP_CHECKSEQUENCEVERIFY
OP_ELSE
  <stk1> CHECKISG <stk2> CHECKISGADD <stk3> CHECKISGADD <stk4> CHECKISGADD <stk5> CHECKISGADD <notary> CHECKISGADD 2 OP_EQUALVERIFY <144 * 455> OP_CHECKSEQUENCEVERIFY
OP_ENDIF

(That’s or_i(and_v(v:multi(3,stk1,stk2,stk3,stk4,stk5,stk6),older(144 * 365)),and_v(v:multi(3,stk1,stk2,stk3,stk4,stk5,stk6),older(144 * 455))).)

Which can be optimized to (while keeping miniscript compatibility):

<stk1> CHECKISG SWAP <stk2> CHECKSIG ADD SWAP <stk3> CHECKSIG ADD SWAP <stk4> CHECKSIG ADD SWAP <stk5> CHECKSIG ADD SWAP <notary> CHECKSIG ADD SWAP
OP_CHECKSIG OP_ADD OP_SWAP OP_IF
  0
OP_ELSE
  <144 * 365> OP_CHECKSEQUENCEVERIFY OP_0NOTEQUAL
OP_ENDIF
OP_ADD OP_SWAP OP_IF
  0
OP_ELSE
  <144 * 455> OP_CHECKSEQUENCEVERIFY OP_0NOTEQUAL
OP_ENDIF

(That’s thresh(4,pk(stk1),s:pk(stk2),s:pk(stk3),s:pk(stk4),s:pk(stk5),s:pk(stk6),sln:older(144 * 365),sln:older(144 * 455)).)

Taproot tree is:

Discussion

OP_VAULT seems like a good fit for this usecase, although a more Taprooty mechanism to add/remove branches from the tree would be preferable. It’d avoid having to use bulk scripts in a single leaf to avoid having to re-trigger a recovery simply to use another path, or avoid being stuck with a perpetually re-triggered recovery. The latter may be mitigated by using longer timelocks than what’s in any the recovery script in every OP_VAULT leaf.

Steven Roose · #2 ·

(Dude this post looks like it’s been formatted with rustfmt. One word per line, really?)

In this specific example it’s probably fine to use the nicer version with multiple OP_VAULT leaves because there is no reason for any two parties to shoot themselves in the foot. It’s not always the case however.

How about the spouse and the kid both paying the notary to keep resetting because they want all the money? :smiley: Inheritance is a game theory nightmare. Jokes aside, I agree with the equivalence :slight_smile: I thought at some point that the semantics were different in another way, that if spouse and kid trigger the unvault and then spouse dies that the money would be locked, but OP_VAULT retains a re-vault, I suppose.

Anthony Towns · #3 · · in reply to #2

You could make those scripts a bit smaller my making them something like:

ie, DUP the keys and use VAULT to put them into the next script, rather than writing them out explicitly.

Anyway, note that the notary can’t keep charging in this scenario – in the first case, the notary enables spouse+notary+455days, then the notary enables child+notary+455days, but that doesn’t disable the spouse+notary+455days path, though it does reset the 455 day counter. On the 455th day, the spouse/child may have to race to get their spend confirmed if they both have notarised signatures, but that’s the extent of it. You could setup the spouse to have 405 days instead, to avoid a race entirely.