OP_VAULT: a new vault proposal
16 messages, 9 to 20 Jan 2023. Started by James O'Beirne.
James O'Beirne (8), Anthony Towns (3), Billy Tetrud (2), rot13maxi, Greg Sanders, Ava Chow
For the last few years, I've been interested in vaults as a way to substantially derisk custodying Bitcoin, both at personal and commercial scales. Instead of abating with familiarity, as enthusiasm sometimes does, my conviction that vaults are an almost necessary part of bitcoin's viability has only grown over the years.
Since people first started discussing vaults, it's been pretty clear that some kind of covenant-enabling consensus functionality is necessary to provide the feature set necessary to make vault use practical.
Earlier last year I experimented with using OP_CTV[1], a limited covenant mechanism, to implement a "minimum-viable" vault design. I found that the inherent limitations of a precomputed covenant scheme left the resulting vault implementation wanting, even though it was an improvement over existing strategies that rely on presigned transactions and (hopefully) ephemeral keys.
But I also found proposed "general" covenant schemes to be unsuitable for this use. The bloated scriptPubKeys, both in size and complexity, that would result when implementing something like a vault weren't encouraging. Also importantly, the social-consensus quagmire regarding which covenant proposal to actually deploy feels at times intractable.
As a result, I wanted to explore a middle way: a design solely concerned with making the best vault use possible, with covenant functionality as a secondary consideration. In other words, a proposal that would deliver the safety benefits of vaults to users without getting hung up on trying to solve the general problem of covenants.
At first this design, OP_VAULT, was just sort of a pipe dream. But as I did more thinking (and eventually implementing) I became more convinced that, even if it isn't considered for soft-fork, it is a worthwhile device to serve as a standard benchmark against which other proposals might be judged.
I wrote a paper that summarizes my findings and the resulting proposal: https://jameso.be/vaults.pdf
along with an accompanying draft implementation: https://github.com/bitcoin/bitcoin/pull/26857
I might work on a BIP if there's interest.
James
Hey James,
Really cool proposal. I’ve been thinking a lot lately about script paths for inheritance. In a lot of the “have a relative time lock that allows a different key to spend coins, or allows a smaller threshold of a multisig to spend” schemes, you have the problem of needing to “refresh” all of your coins when the timelock is close to maturation. In a lot of the “use multisig with ephemeral keys to emulate covenants” schemes, you have to pre-commit to the terminal destination well in advance of the spend-path being used, which leads to all kinds of thorny questions about security and availability of *those* keys. In other words, you either have to have unbound destinations but a timer that needs resetting, or you have unbound time but fixed destinations. This design gets you the best of both because the destination SPKs aren’t committed to until the unvaulting process starts. This (or something like this with destination binding at unvault-time) would be an incredibly useful tool for inheritance designs in wallets.
I need to think a bit more about the recovery path not having any real encumbrances on it. Maybe in practice if you’re worried about DoS, you have UTXOs that commit to multiple vault paths that have tweaked recovery destinations or something, or maybe it really is the right move to say that if recovery is triggered, you probably do want it for all of your inflight unvaultings.
Looking forward to reading this a few more times and talking more about it.
Thanks! rijndael
On Mon, Jan 9, 2023 at 11:07 AM, James O'Beirne via bitcoin-dev <bitcoin-dev@lists•linuxfoundation•org> wrote:
Quoted text (46 lines)
> For the last few years, I've been interested in vaults as a way to > substantially derisk custodying Bitcoin, both at personal and commercial > scales. Instead of abating with familiarity, as enthusiasm sometimes > does, my conviction that vaults are an almost necessary part of bitcoin's > viability has only grown over the years. > > Since people first started discussing vaults, it's been pretty clear that > some kind of covenant-enabling consensus functionality is necessary to > provide the feature set necessary to make vault use practical. > > Earlier last year I experimented with using OP_CTV[1], a limited covenant > mechanism, to implement a "minimum-viable" vault design. I found that the > inherent limitations of a precomputed covenant scheme left the resulting > vault implementation wanting, even though it was an improvement over > existing strategies that rely on presigned transactions and (hopefully) > ephemeral keys. > > But I also found proposed "general" covenant schemes to be > unsuitable for this use. The bloated scriptPubKeys, both in size and > complexity, that would result when implementing something like a vault > weren't encouraging. Also importantly, the social-consensus quagmire > regarding which covenant proposal to actually deploy feels at times > intractable. > > As a result, I wanted to explore a middle way: a design solely concerned > with making the best vault use possible, with covenant functionality as a > secondary consideration. In other words, a proposal that would deliver > the safety benefits of vaults to users without getting hung up on > trying to solve the general problem of covenants. > > At first this design, OP_VAULT, was just sort of a pipe dream. But as I > did more thinking (and eventually implementing) I became more convinced > that, even if it isn't considered for soft-fork, it is a worthwhile > device to serve as a standard benchmark against which other proposals > might be judged. > > I wrote a paper that summarizes my findings and the resulting proposal: > https://jameso.be/vaults.pdf > > along with an accompanying draft implementation: > https://github.com/bitcoin/bitcoin/pull/26857 > > I might work on a BIP if there's interest. > > James > [1]: https://github.com/jamesob/simple-ctv-vault
Hi James and co,
Currently there is no way to make this compatible with scripthashes of any kind, since the script interpreter has no insight into the OP_UNVAULT outputs' "execution script", and one of the arguments of OP_UNVAULT is freeform, resulting in an unpredictable output scriptpubkey.
I think the fix is just requiring a single additional witness data item during OP_VAULT spend(for unvault path), mandating the <target-outputs-hash> to be included in the witness stack as an input to OP_VAULT opcode, and transaction introspection then checks to make sure the witness item and the corresponding output script template matches the expected.
This would only be necessary for the unvaulting path, and not for the recovery path.
Cheers, Greg
On Mon, Jan 9, 2023 at 2:10 PM rot13maxi via bitcoin-dev < bitcoin-dev@lists•linuxfoundation•org> wrote:
Quoted text (88 lines)
> Hey James, > > Really cool proposal. I’ve been thinking a lot lately about script paths > for inheritance. In a lot of the “have a relative time lock that allows a > different key to spend coins, or allows a smaller threshold of a multisig > to spend” schemes, you have the problem of needing to “refresh” all of your > coins when the timelock is close to maturation. In a lot of the “use > multisig with ephemeral keys to emulate covenants” schemes, you have to > pre-commit to the terminal destination well in advance of the spend-path > being used, which leads to all kinds of thorny questions about security and > availability of *those* keys. In other words, you either have to have > unbound destinations but a timer that needs resetting, or you have unbound > time but fixed destinations. This design gets you the best of both because > the destination SPKs aren’t committed to until the unvaulting process > starts. This (or something like this with destination binding at > unvault-time) would be an incredibly useful tool for inheritance designs in > wallets. > > I need to think a bit more about the recovery path not having any real > encumbrances on it. Maybe in practice if you’re worried about DoS, you have > UTXOs that commit to multiple vault paths that have tweaked recovery > destinations or something, or maybe it really is the right move to say that > if recovery is triggered, you probably do want it for all of your inflight > unvaultings. > > Looking forward to reading this a few more times and talking more about > it. > > Thanks! > rijndael > > > On Mon, Jan 9, 2023 at 11:07 AM, James O'Beirne via bitcoin-dev < > bitcoin-dev@lists•linuxfoundation•org> wrote: > > For the last few years, I've been interested in vaults as a way to > substantially derisk custodying Bitcoin, both at personal and commercial > scales. Instead of abating with familiarity, as enthusiasm sometimes > does, my conviction that vaults are an almost necessary part of bitcoin's > viability has only grown over the years. > > Since people first started discussing vaults, it's been pretty clear that > some kind of covenant-enabling consensus functionality is necessary to > provide the feature set necessary to make vault use practical. > > Earlier last year I experimented with using OP_CTV[1], a limited covenant > mechanism, to implement a "minimum-viable" vault design. I found that the > inherent limitations of a precomputed covenant scheme left the resulting > vault implementation wanting, even though it was an improvement over > existing strategies that rely on presigned transactions and (hopefully) > ephemeral keys. > > But I also found proposed "general" covenant schemes to be > unsuitable for this use. The bloated scriptPubKeys, both in size and > complexity, that would result when implementing something like a vault > weren't encouraging. Also importantly, the social-consensus quagmire > regarding which covenant proposal to actually deploy feels at times > intractable. > > As a result, I wanted to explore a middle way: a design solely concerned > with making the best vault use possible, with covenant functionality as a > secondary consideration. In other words, a proposal that would deliver > the safety benefits of vaults to users without getting hung up on > trying to solve the general problem of covenants. > > At first this design, OP_VAULT, was just sort of a pipe dream. But as I > did more thinking (and eventually implementing) I became more convinced > that, even if it isn't considered for soft-fork, it is a worthwhile > device to serve as a standard benchmark against which other proposals > might be judged. > > I wrote a paper that summarizes my findings and the resulting proposal: > https://jameso.be/vaults.pdf > > along with an accompanying draft implementation: > https://github.com/bitcoin/bitcoin/pull/26857 > > I might work on a BIP if there's interest. > > James > > [1]: https://github.com/jamesob/simple-ctv-vault > > _______________________________________________ > bitcoin-dev mailing list > bitcoin-dev@lists•linuxfoundation•org > https://lists.linuxfoundation.org/mailman/listinfo/bitcoin-dev >
Hey Greg,
I think what you're trying to get at here is that the OP_UNVAULT scriptPubKey *must* be a bare script so that the OP_VAULT spend logic can verify that we're spending an OP_VAULT output into a compatible OP_UNVAULT output, and that's true. The OP_UNVAULT scriptPubKey also must contain the target hash because that has is used when validating that spend to ensure that the final unvault target matches what was advertised when the OP_UNVAULT output was created.
So I'm not sure what problem you're trying to solve by putting the target hash on the OP_VAULT spend witness stack. If it were placed there, it wouldn't be accessible during OP_UNVAULT spend AFAICT. I agree it would be nice to figure out a way to allow the OP_UNVAULT scriptPubKey to not be bare, which may require moving the target hash out of it, but we'd have to figure out a mechanism to properly forward the target hash for validation.
Best, James
On Mon, Jan 9, 2023 at 2:32 PM Greg Sanders <gsanders87@gmail•com> wrote:
Quoted text (112 lines)
> Hi James and co, > > Currently there is no way to make this compatible with scripthashes of any > kind, since the script interpreter has no insight into the OP_UNVAULT > outputs' "execution script", and one of the arguments of OP_UNVAULT is > freeform, resulting in an unpredictable output scriptpubkey. > > I think the fix is just requiring a single additional witness data item > during OP_VAULT spend(for unvault path), mandating the > <target-outputs-hash> to be included in the witness stack as an input to > OP_VAULT opcode, and transaction introspection then checks to make sure the > witness item and the corresponding output script template matches the > expected. > > This would only be necessary for the unvaulting path, and not for the > recovery path. > > Cheers, > Greg > > On Mon, Jan 9, 2023 at 2:10 PM rot13maxi via bitcoin-dev < > bitcoin-dev@lists•linuxfoundation•org> wrote: > >> Hey James, >> >> Really cool proposal. I’ve been thinking a lot lately about script paths >> for inheritance. In a lot of the “have a relative time lock that allows a >> different key to spend coins, or allows a smaller threshold of a multisig >> to spend” schemes, you have the problem of needing to “refresh” all of your >> coins when the timelock is close to maturation. In a lot of the “use >> multisig with ephemeral keys to emulate covenants” schemes, you have to >> pre-commit to the terminal destination well in advance of the spend-path >> being used, which leads to all kinds of thorny questions about security and >> availability of *those* keys. In other words, you either have to have >> unbound destinations but a timer that needs resetting, or you have unbound >> time but fixed destinations. This design gets you the best of both because >> the destination SPKs aren’t committed to until the unvaulting process >> starts. This (or something like this with destination binding at >> unvault-time) would be an incredibly useful tool for inheritance designs in >> wallets. >> >> I need to think a bit more about the recovery path not having any real >> encumbrances on it. Maybe in practice if you’re worried about DoS, you have >> UTXOs that commit to multiple vault paths that have tweaked recovery >> destinations or something, or maybe it really is the right move to say that >> if recovery is triggered, you probably do want it for all of your inflight >> unvaultings. >> >> Looking forward to reading this a few more times and talking more about >> it. >> >> Thanks! >> rijndael >> >> >> On Mon, Jan 9, 2023 at 11:07 AM, James O'Beirne via bitcoin-dev < >> bitcoin-dev@lists•linuxfoundation•org> wrote: >> >> For the last few years, I've been interested in vaults as a way to >> substantially derisk custodying Bitcoin, both at personal and commercial >> scales. Instead of abating with familiarity, as enthusiasm sometimes >> does, my conviction that vaults are an almost necessary part of bitcoin's >> viability has only grown over the years. >> >> Since people first started discussing vaults, it's been pretty clear that >> some kind of covenant-enabling consensus functionality is necessary to >> provide the feature set necessary to make vault use practical. >> >> Earlier last year I experimented with using OP_CTV[1], a limited covenant >> mechanism, to implement a "minimum-viable" vault design. I found that the >> inherent limitations of a precomputed covenant scheme left the resulting >> vault implementation wanting, even though it was an improvement over >> existing strategies that rely on presigned transactions and (hopefully) >> ephemeral keys. >> >> But I also found proposed "general" covenant schemes to be >> unsuitable for this use. The bloated scriptPubKeys, both in size and >> complexity, that would result when implementing something like a vault >> weren't encouraging. Also importantly, the social-consensus quagmire >> regarding which covenant proposal to actually deploy feels at times >> intractable. >> >> As a result, I wanted to explore a middle way: a design solely concerned >> with making the best vault use possible, with covenant functionality as a >> secondary consideration. In other words, a proposal that would deliver >> the safety benefits of vaults to users without getting hung up on >> trying to solve the general problem of covenants. >> >> At first this design, OP_VAULT, was just sort of a pipe dream. But as I >> did more thinking (and eventually implementing) I became more convinced >> that, even if it isn't considered for soft-fork, it is a worthwhile >> device to serve as a standard benchmark against which other proposals >> might be judged. >> >> I wrote a paper that summarizes my findings and the resulting proposal: >> https://jameso.be/vaults.pdf >> >> along with an accompanying draft implementation: >> https://github.com/bitcoin/bitcoin/pull/26857 >> >> I might work on a BIP if there's interest. >> >> James >> >> [1]: https://github.com/jamesob/simple-ctv-vault >> >> _______________________________________________ >> bitcoin-dev mailing list >> bitcoin-dev@lists•linuxfoundation•org >> https://lists.linuxfoundation.org/mailman/listinfo/bitcoin-dev >> >
On Mon, Jan 09, 2023 at 11:07:54AM -0500, James O'Beirne via bitcoin-dev wrote:
Quoted text (2 lines)
> But I also found proposed "general" covenant schemes to be > unsuitable for this use. The bloated scriptPubKeys,
I don't think that makes sense? With a general scheme, you'd only be bloating the witness data (perhaps including the witness script) not the scriptPubKey?
Terminology suggestion: instead of calling it the "recovery" path, call it "freezing your funds". Then you're using your "hot wallet" (aka unvault-spk-hash) for the unvault path for all your normal transactions, but if there's a problem, you freeze your funds, and they're now only accessible via your "cold wallet" (aka recovery-spk-hash).
As I understand it, your scheme is:
scriptPubKey: <vault tag> <#recovery> (<delay> <#unvault>)
where #recovery is the sha256 hashes of an arbitrary scriptPubKey, #unvault is the sha256 hash of a witness script, and delay is a relative block count.
This scriptPubKey allows for two spend paths:
recovery: spends directly to <recovery>; verified by checking it
the hash of the sPK matches <#recovery> and the amount it preserved
unvaulting:
spends to a scriptPubKey of <unvault tag> <#recovery> (<delay> <#target>)
verified by checking that the witness script hashes to #unvault and
is satisfied, and #target is a CTV-ish commitment of the eventual
withdrawal (any CHECKSIG operations in the unvault witness script will
commit to the sPK output, preventing this from being modified by
third parties). #recovery and delay must match the values from the
vault scriptPubKey.
The unvault scriptPubKey likewise likewise allows for two spend paths:
recovery: same as above
withdrawal:
verifies that all the outputs hash to #target, and that nSequence
is set to a relative timelock of at least delay.
This means that as soon as your recovery address (the preimage to #recovery) is revealed, anyone can move all your funds into cold storage (presuming they're willing to pay the going feerate to do so). I think this is a feature, not a bug, though: if your hot wallet is compromised, moving all your funds to cold storage is desirable; and if you want to have different hot wallets with a single cold wallet, then you can use a HD cold-wallet so that revealing one address corresponding to one hot wallet, doesn't reveal the addresses corresponding to other hot wallets. (This is addressed in the "Denial-of-service protection" section)
It does however mean that the public key for your cold wallet needs to be handled secretly though -- if you take the cold wallet xpub and send it to a random electrum server to check your cold wallet balance, that would allow a malicious party to lock up all your funds.
I think it might be better to use a pay-to-contract construction for the recovery path, rather than an empty witness. That is, take your recovery address R, and calculate #recovery=sha256(R, sha256(secret)) (where "secret" is something derived from the R's private key, so that it can be easily recovered if you only have your cold wallet and lose all your metadata). When you want to recover all your funds to address R, you reveal sha256(secret) in the witness data and R in the scriptPubKey, OP_VAULT hashes these together and checks the result matches #recovery, and only then allows it. That would allow you to treat R as public knowledge, without risking your funds getting randomly frozen.
This construct allows delayed withdrawals (ie "the cold wallet can withdraw instantly, the hot wallet can withdraw only after delay blocks"), but I don't think it provides any way to cap withdrawals ("the cold wallet can withdraw all funds, the hot wallet can only withdraw up to X funds per day/week"). Having a fixed limit probably isn't compatible with having a multi-utxo vault ("you can withdraw $10k per day" doesn't help if your $5M is split across 500 $10k utxos, and the limit is only enforced per-utxo), but I think a percentage limit would be.
I think a generic OP_UNVAULT can be used to simulate OP_CTV: replace "<h> OP_CTV" with "<000..0> 0 <h> OP_UNVAULT". The paper seems to put "OP_UNVAULT" first, but the code seems to expect it to come last, not sure what's up with that inconsistency.
I'm not sure why you'd want a generic opcode though; if you want the data to be visible in the scriptPubKey, you need to use a new segwit version with structured data, anyway; so why not just do that?
I think there's maybe a cleverer way of batching / generalising checking that input/output amounts match. That is, rather than just checking that "the input's a vault; so the corresponding output must be one of these possibilities, and the input/output values must exactly match", that it's generalised to be:
* set A = the sum of each input that's taking the unvaulting path
from a vault scriptPubKey with #recovery=X
* set B = the sum of each output that has an unvault tag with
#recovery=X
* set C = the sum of each output that has a vault tag with
#recovery=X
* check that A=B+C
(That allows consolidation of your vault via your hot wallet, just by not having any unvault outputs, so B=0. I suspect that if you allowed for keyless consolidation of your vault, that that would be a griefing/DoS vector)
This differs from the actual proposal, AIUI, which instead requires that there are just two outputs - an ephemeral anchor for attaching fees, and the primary vault or unvault output, and that all the inputs are vaulting/unvaulting txs.
I think one meaningful difference between these two approaches is that the current proposal means unvaulting locks up the entire utxo for the delay period, rather than just the amount you're trying to unvault. eg, if you have a single vault utxo X with 1000 BTC, you have delay set to 1008 blocks (1 week), and you decide on Tuesday that you wish to withdraw 50 BTC, creating an unvault tx spending 50 BTC somewhere and 950 BTC back to your vault, you can't spend any of the 950 BTC for another two weeks: one week for the unvault to confirm and it to go back into your vault, and another week for the next unvault to confirm.
Changing the unvault construction to have an optional OP_VAULT output would remedy that, I think.
It would be fairly dangerous to combine a construction like this (which encourages the vault sPK to be reused) with APO signatures on the hot wallet -- in that case the signature could just be replayed against a different vault utxo, and you'd be paying for things twice. But provided that vault spends are only (1) signed by the hot wallet, or (2) being frozen and moved to the recovery sPK, then you should have complete control over your utxos/coins, and using APO probably isn't interesting anyway.
What would it look like to just hide all this under taproot?
First you'd just leave the internal pubkey as your cold wallet key (or a NUMS point if your cold wallet is complicated).
Working backwards, your unvault output needs two script paths:
1) move_funds_to(recovery-spk) 2) <D> OP_CSV; move_funds_to(X)
Your vault output also needs two paths:
1) move_funds_to(recovery-spk) 2) hot-wallet-script; move_funds_to(unvault[X])
That obviously requires a "move_funds_to" operator, which, using liquid's operators (roughly), could be something like:
PUSHCURRENTINPUTINDEX DUP2 INSPECTINPUTVALUE SWAP INSPECTOUTVALUE EQUALVERIFY INSPECTOUTPUTSCRIPTPUBKEY "x" EQUAL
which is just ~8 bytes overhead, or could perhaps be something fancier that supports the batching/consolidation abilities discussed above.
It also needs some way of constructing "unvault[X]", which could be a TLUV-like construction.
That all seems possible to me; though certainly needs more work/thought than just having dedicated opcodes and stuffing the data directly in the sPK.
Cheers, aj
Greg explained his suggestion to me off-list, and I think it's a good one. To summarize, consider the normal "output flow" of an expected vault use:
(i) output to be vaulted
=> (ii) OP_VAULT output
=> (iii) OP_UNVAULT "trigger" output
=> (iv) final output
In my existing draft implementation, all outputs aside from (iii), the OP_UNVAULT trigger, can be P2TR or P2WSH. In other words, those outputs can hide their true script until spend. In my draft, the OP_UNVAULT trigger had to be bare so that the script interpreter could inspect part of it for validity: "does this OP_UNVAULT have the same <recovery-spk-hash> and <spend-delay> as the OP_VAULT?"
If that output wasn't bare, because the <target-hash> is variable at the time of OP_UNVAULT output creation, the script interpreter would have no way of constructing the expected scriptPubKey.
Greg's suggestion would allow that output to be any kind of script. He suggests to put the <target-hash> onto the witness stack when spending the OP_VAULT output (and creating the OP_UNVAULT output). If we did that, the script interpreter could e.g. use a NUMS point (i.e. a publicly known point with no usable private key) to construct a Taproot configuration that looks like
tr(NUMS, {<OP_UNVAULT <recovery-key> <spend-delay> <target-hash>})
and check if the scriptPubKey of the proposed OP_UNVAULT output matches that. This would allow all outputs in vault lifecycles to be P2TR, for example, which would conceal the operation of the vault - a very nice feature!
This would also allow the OP_VAULT/OP_UNVAULT opcodes to be implemented as Taproot-only OP_SUCCESSx opcodes, if that was decided to be preferable.
The problem is how to (and whether to) enable something similar for witness v0 outputs. For example, if we want the (ii) and (iii) output scripts to live behind P2WSH. One (kind of hacky) option to enable this is to have the script interpreter construct the expected OP_UNVAULT scriptPubKey on the basis of what witness version it sees. For example, if it sees "OP_0 <32 bytes data>", it would use <target-hash> on the witness stack to construct a fitting P2WSH scriptPubKey that is compatible with the OP_VAULT being spent, and then match against that. But if it detects "OP_1 <32 bytes data>", it would do the same process for an expected Taproot-with-NUMS output.
---
Anyway, sorry if that was more verbose than necessary, but I think it's a really great suggestion from Greg. I'll look at modifying the implementation accordingly. I'd be curious to hear what others think as well.
Forwarding in some conceptual feedback from the pull request.
From ariard:
Quoted text (1 line)
> I've few open questions, like if the recovery path should be committed
with a signature rather than protected by a simple scriptpubkey preimage.
That's something I've wondered about too. I have to ruminate on AJ's good post about this, but a pretty straightforward way of enabling this (at the expense of some complexity) is to do something like "if <recovery-path-hash> is 32 bytes, treat it as it's currently used. If it's 33 bytes, use the first byte as a parameter for how to interpret it." To start with, an extra prefix byte of 0x00 could mean "require a witness satisfying the scriptPubKey that hashes to the remaining 32 bytes" in the same way we do the unvault signing. This would enable a "sign-to-recover" flow at the option of the user, specified during vault creation.
Quoted text (1 line)
> The current OP_VAULT implementation is using OP_NOP repurposing but this
doesn't seem compatible with Taproot-only extensions (e.g ANYPREVOUT) and maybe a OP_SUCCESS could be used.
Yes, with Greg's suggestion of putting <target-hash> on the witness stack during OP_VAULT (-> OP_UNVAULT) spend, we could conceivably move OP_VAULT/OP_UNVAULT into Taproot-only OP_SUCCESSx opcodes. I haven't thought hard about how worthwhile it is to preserve the ability to use OP_VAULT in pre-Taproot contexts.
Quoted text (1 line)
> There is a conceptual wonder, if a CTV and template malleability approach
wouldn't better suit the vault use-case and allow other ones, as such better re-usability of primitives.
I dedicated a whole section of the paper ("Precomputed vaults with covenants") to explaining why precomputed covenant mechanisms have big shortcomings for vaults.
That said, a number of people have commented about OP_VAULT's ability to (inefficiently) emulate CTV. I'm still very supportive of CTV, I just don't really have any uses I personally understand inside and out aside from vaults... so if others do, they should really post about it on this list and we should resume working on an activation for CTV!
---
From naumenkogs:
Quoted text (1 line)
> I'm personally not sure batching withdrawals is that compelling... It's a
nice-to-have, but I'd think about the benefits dropping this feature would provide.
Having familiarity with a few large-scale custodial operations, I think batching is a really big deal. And if you're going to support multiple deposits to the same vault, no support for batching is going to result in a lot of unnecessary output creation even as a small user if you're, e.g., doing weekly automated deposits from an exchange to a vault you've configured.
Darosior comments:
Quoted text (1 line)
> On the contrary i think the batching feature is very compelling. The
impossibility to batch Unvaults in Revault is a major drawback: it significantly increases the cost of any realistic operation (you need one whole additional transaction per input, and each have likely more than one output). It also potentially increases the cost on the network (you'd likely want some sort of anchor output on each Unvault tx, that you might not spend, so that's 2*n outputs created with n the number of coins spent): we definitely don't want to prevent batching. The ability to batch the recovery transactions (what we called Emergency tx in Revault) is also very compelling but i think your comment was only about batched withdrawals.
Thanks for the thoughtful reply AJ.
Quoted text (3 lines)
> I don't think that makes sense? With a general scheme, you'd only be > bloating the witness data (perhaps including the witness script) not > the scriptPubKey?
Sorry, sloppy language on my part. To be charitable, I'm talking about the "figurative sPK," which of course these days lives in the witness for script-path-ish spends. Maybe the witness discount means that "complicated" scripts aren't as big a deal, depending on the actual difference in raw script size.
Quoted text (2 lines)
> I think it might be better to use a pay-to-contract construction for > the recovery path, rather than an empty witness.
So I guess the one advantage that what you're proposing has over just using a recovery-path key signature is that it's all derivable from your cold privkey; you don't have to worry about accidentally losing the recovery-path key.
Of course you're still vulnerable to spurious sweeps if the sha256(secret) value gets found out, which presumably you'd want in an accessible cache to avoid touching the cold secret every time you want to sweep.
What do you think about the idea of making the recovery-path authorization behavior variable on a single byte flag preceding the 32 byte data push, as I mentioned in another post? I think it may make sense to leave this option open to end-users (and also allow for some upgradeability).
Quoted text (2 lines)
> I think a generic OP_UNVAULT can be used to simulate OP_CTV: replace > "<h> OP_CTV" with "<000..0> 0 <h> OP_UNVAULT".
Yup, that's an inefficient way of emulating CTV. If people want CTV, we should just look at activating CTV. Greg Sanders has a thing about "jetting" CTV into this proposal (I think) so that the code-paths are shared, but I haven't figured out how that would work. They really don't share that much code AFAICT.
Quoted text (2 lines)
> The paper seems to put "OP_UNVAULT" first, but the code seems to > expect it to come last, not sure what's up with that inconsistency.
Again some sloppy notation on my part. What I sort of meant in the paper was a kind of functional notation `OP_VAULT(param1, param2, ...)`. Let's chalk that up to my inexperience actually working on script stuff.
Quoted text (7 lines)
> I think there's maybe a cleverer way of batching / generalising > checking that input/output amounts match. > > [...] > > * set C = the sum of each output that has a vault tag with > #recovery=X
This would also need to take into account that the <spend-delay>s are compatible, but your point is well taken.
Quoted text (3 lines)
> I think one meaningful difference between these two approaches is that > the current proposal means unvaulting locks up the entire utxo for the > delay period, rather than just the amount you're trying to unvault.
This is a really good point and I think is one that's important to incorporate with a change to the existing proposal.
A simple fix for facilitating the use of a "partial revault" while the OP_UNVAULT UTXO is outstanding would be to allow for an optional third output that is a redeposit back to the identical OP_VAULT sPK that is being spent by the OP_UNVAULT transaction, then the script interpreter would just ensure that the nValue of those two outputs sums to the sum of the input nValues.
I can see what you're saying about having more generic "group amounts by compatible vault params, and then compare to similarly grouped outputs," but I'm just wondering if there are other uses that enables besides the partial-revault thing I mentioned above. If not, I'd probably rather just stick with something simple like having the third optional re-vault output.
Quoted text (2 lines)
> Changing the unvault construction to have an optional OP_VAULT output > would remedy that, I think.
Oh - okay, this is what you're saying. Right!
Is that a sufficient change, or are there other benefits that the more complicated clever-I/O-vault-grouping would enable that you have in mind?
Quoted text (10 lines)
> What would it look like to just hide all this under taproot? > > [...] > > It also needs some way of constructing "unvault[X]", which could be a > TLUV-like construction. > > That all seems possible to me; though certainly needs more work/thought > than just having dedicated opcodes and stuffing the data directly in > the sPK.
I think this is a very important comparison to do, but I'm eager to see code for things like this. There have been a lot of handwavey proposals lately without tangible code artifacts. I'm eager to see what these alternatives look like in practice - i.e. in functional tests.
Thanks again for the great mail. James
On Tue, Jan 10, 2023 at 03:22:54PM -0500, James O'Beirne wrote:
Quoted text (8 lines)
> > I don't think that makes sense? With a general scheme, you'd only be > > bloating the witness data (perhaps including the witness script) not > > the scriptPubKey? > Sorry, sloppy language on my part. To be charitable, I'm talking about > the "figurative sPK," which of course these days lives in the witness > for script-path-ish spends. Maybe the witness discount means that > "complicated" scripts aren't as big a deal, depending on the actual > difference in raw script size.
Sure. I think there's three aspects that matter for the witness script:
1) it shouldn't be long/costly to do things that are common and easy:
if you can express "OP_VAULT" in ~70 bytes, you shouldn't have to
spend 1000 bytes to do so; if it can be cheap to validate, you
shouldn't have to pay 100x markup in fees to use it. With the
exception of things that build up from basics (like
CAT/CHECKSIGFROMSTACK approaches), I think this is mostly fine
though.
2) once someone figures out a design, it should be easy to reuse;
but I think that's not a big deal: you just write up a spec for
your script, and people use that in their different wallet software,
much like the specialised scripts for lightning HTLCs
3) primitives should be designed to be easy to safely build on and
scripts should be as easy as possible to analyse once written;
ie, we want things more like miniscript than "The story of Mel,
a Real Programmer"
With some caveats (like that using the cold wallet xpub to scan the blockchain before you've frozen all your funds is dangerous), OP_VAULT seems really good on all those fronts, of course.
Quoted text (10 lines)
> > I think it might be better to use a pay-to-contract construction for > > the recovery path, rather than an empty witness. > So I guess the one advantage that what you're proposing has over just > using a recovery-path key signature is that it's all derivable from your > cold privkey; you don't have to worry about accidentally losing the > recovery-path key. > Of course you're still vulnerable to spurious sweeps if the > sha256(secret) value gets found out, which presumably you'd want in an > accessible cache to avoid touching the cold secret every time you want > to sweep.
Sure, "sha256(secret)" itself needs to be semi-secret -- it allows anyone who knows it to freeze your funds, even if it doesn't allow anyone to steal them. You could presumably do all the usual things to protect that secret: split it up with secret sharing; put it in a hardware wallet; keep it offline; etc.
Quoted text (3 lines)
> What do you think about the idea of making the recovery-path > authorization behavior variable on a single byte flag preceding the 32 > byte data push, as I mentioned in another post?
] "if ] <recovery-path-hash> is 32 bytes, treat it as it's currently used. If it's ] 33 bytes, use the first byte as a parameter for how to interpret it." To ] start with, an extra prefix byte of 0x00 could mean "require a witness ] satisfying the scriptPubKey that hashes to the remaining 32 bytes" in the ] same way we do the unvault signing.
I don't think 33 bytes would be enough? There isn't really a way to commit to the recovery destination within the script? So I think you'd need "<32 byte recovery-path-hash><n byte scriptPubKey>"
Aside from that, my opinion's one/all of:
a) sounds fine
b) maybe you could just always have it include a scriptPubKey? for the times when you just want "reveal the cold wallet preimage" just have the scriptPubKey be the single byte "OP_TRUE"; for the times when you it to be "reveal random preimage" you'd have it be the 22 byte "HASH160 <hash160(sha256(secret))> EQUAL"?
c) delegation to a script is a great idea, that's come up multiple times (OP_EVAL, BIP117, graftroot) -- it's probably better to have it available as a generic feature, than bolted on to particular features
Quoted text (3 lines)
> > I think a generic OP_UNVAULT can be used to simulate OP_CTV: replace > > "<h> OP_CTV" with "<000..0> 0 <h> OP_UNVAULT". > Yup, that's an inefficient way of emulating CTV.
Sure; I think it's only interesting in establishing how powerful the construct is in the abstract. It's not an exact match for CTV since it hashes some things differently.
I don't really think it's necessarily that inefficient fwiw; "0 SHA256 0 <h> UNVAULT" is only 3 more bytes than "<h> CTV", could give you an unspendable recovery path, provided UNVAULT wants either a BIP341 tagged hash (which is what the implementation does, by the looks), or a HASH256 for the recovery path.
(Again, this assumes UNVAULT is available in script, and isn't just a special scriptPubKey type)
Quoted text (7 lines)
> > I think there's maybe a cleverer way of batching / generalising > > checking that input/output amounts match. > > [...] > > * set C = the sum of each output that has a vault tag with > > #recovery=X > This would also need to take into account that the <spend-delay>s are > compatible, but your point is well taken.
Sure, I guess output/UNVAULT delay >= input/VAULT delay would be sufficient for that.
I guess having all that stuff exposed in the scriptPubKey would be slightly annoying for griefing -- you set your wallet delay to be 1008 blocks, and someone sends to your vault with a 1007 block delay, or a 6 block delay: does your wallet notice the tx? if it does, what do you do with those funds?
Maybe that would be a(nother) good reason to hide the OP_VAULT side of things in taproot (or p2wsh): then you just have a (shorter) taproot sPK that encodes both things, and people normally don't even have enough information to correctly tweak your sPK to have a different delay.
Quoted text (2 lines)
> I can see what you're saying about having more generic "group amounts by > compatible vault params, and then compare to similarly grouped outputs,"
I mostly wrote that up because that's what I imagined your batching doing before I'd finished reading...
A big disadvantage of that approach compared to yours is that you have to analyse a potentially arbitrarily large transaction as a whole while validating each input -- this input uses recovery key XXX which output's match; this input uses recovery key YYY; oh, this one uses XXX again; etc. With your approach, you only have to look at particular outputs.
An advantage of that extra complexity is that you could combine operations from multiple different vaults into a single transaction, potentially along with fees or a coinjoin or whatever else. Maybe that would be interesting if this were something you could code in script via some generic opcodes; but here it would be extra complexity in consensus code, and it doesn't seem like a good match for OP_VAULT's design parameters.
Cheers, aj
Hi James,
This seems like a promising proposal, but I noticed have a few issues regarding batching and privacy.
It seems like this proposal will encourage address reuse for vaults, at least in some parts. It seems like it would not be difficult to ensure that each vault address was unique through the use of key derivation. The recovery and unvault scripts could be produced from ranged descriptors and so there would each vault address would be unique as each recovery and unvault script is unique. It would not be hard to have descriptors for vaults, which would then allow for usage of other descriptors and miniscript into the recovery and unvault scripts.
However the current construction makes it impossible to spend these vaults together. Since OP_VAULT requires the recovery script of the unvault output to match what's provided in the input, if there are multiple inputs with different recovery scripts, then the transaction will fail. I'm not sure how this could be solved though.
But from my reading of the code, it looks like the unvault scripts can be unique, so at least address reuse can be avoided here. It just means that the recovery scripts must be the same, and this would leave an identifying mark on chain for every unvault. An observer would be able to correlate unvault transactions by the hashes of the recovery scripts, and I think this would be rather detrimental to user privacy, not to mention that sweeping to recovery would also reveal all of your coins too.
On the topic of address reuse, the implemented optional re-vault output explicitly requires address reuse, as well as breaking the batched unvaulting of scripts that have different unvault scripts. It's currently implemented as requiring the unvault script to exactly match the prevout script of the inputs being spent. This means that all inputs must have the same script. I think it would be sufficient to do the same check as the OP_UNVAULT script and just require that the recovery script and the delay are the same, with the hash of the trigger script being provided in the input in the same way the target hash is provided for OP_UNVAULT. This would break the address reuse requirement.
I'm also not convinced that OP_VAULT and OP_UNVAULT should be allowed for bare and P2WSH outputs. It seems like it would make sense to just limit their usage to tapscripts as this would simply their implementation.
Andrew
On 01/09/2023 11:07 AM, James O'Beirne via bitcoin-dev wrote:
Quoted text (47 lines)
> For the last few years, I've been interested in vaults as a way to > substantially derisk custodying Bitcoin, both at personal and commercial > scales. Instead of abating with familiarity, as enthusiasm sometimes > does, my conviction that vaults are an almost necessary part of bitcoin's > viability has only grown over the years. > > Since people first started discussing vaults, it's been pretty clear that > some kind of covenant-enabling consensus functionality is necessary to > provide the feature set necessary to make vault use practical. > > Earlier last year I experimented with using OP_CTV[1], a limited covenant > mechanism, to implement a "minimum-viable" vault design. I found that the > inherent limitations of a precomputed covenant scheme left the resulting > vault implementation wanting, even though it was an improvement over > existing strategies that rely on presigned transactions and (hopefully) > ephemeral keys. > > But I also found proposed "general" covenant schemes to be > unsuitable for this use. The bloated scriptPubKeys, both in size and > complexity, that would result when implementing something like a vault > weren't encouraging. Also importantly, the social-consensus quagmire > regarding which covenant proposal to actually deploy feels at times > intractable. > > As a result, I wanted to explore a middle way: a design solely concerned > with making the best vault use possible, with covenant functionality as a > secondary consideration. In other words, a proposal that would deliver > the safety benefits of vaults to users without getting hung up on > trying to solve the general problem of covenants. > > At first this design, OP_VAULT, was just sort of a pipe dream. But as I > did more thinking (and eventually implementing) I became more convinced > that, even if it isn't considered for soft-fork, it is a worthwhile > device to serve as a standard benchmark against which other proposals > might be judged. > > I wrote a paper that summarizes my findings and the resulting proposal: > https://jameso.be/vaults.pdf > > along with an accompanying draft implementation: > https://github.com/bitcoin/bitcoin/pull/26857 > > I might work on a BIP if there's interest. > > James > > [1]: https://github.com/jamesob/simple-ctv-vault
On Mon, Jan 16, 2023 at 11:47:09PM +0000, Andrew Chow via bitcoin-dev wrote:
Quoted text (1 line)
> It seems like this proposal will encourage address reuse for vaults,
(That is listed as an explicit goal: "A single vault scriptPubKey should be able to "receive" multiple deposits")
Quoted text (3 lines)
> However the current construction makes it impossible to spend these > vaults together. Since OP_VAULT requires the recovery script of the > unvault output to match what's provided in the input,
I don't think this part is a big problem -- the recovery path requires revealing a secret, but if you separate that secret from the recovery path sPK, you could vary the secret. ie:
unvault1 delay recovery1 VAULT unvault2 delay recovery2 VAULT
where recovery1 = SHA256(SHA256(secret1), rSPK) and recovery2 = SHA256(SHA256(secret2), rSPK), and both are spendable when the top stack element is secretN and the first output pays at least the sum of all the OP_VAULT using inputs to rSPK. So batched recovery could work fine, I think.
(If you're using the same "recovery" parameter to each VAULT, then you're revealing which txs are in your vault at spend time, rather than at receive time, which doesn't seem all that much better to me)
But the problem with this is it prevents you from combining vaults when spending normally: so if you've got a bunch of vaults with 1 BTC each, and want to spend 10 BTC on a house, you'll need to make 11 separate transactions:
* 10 txs each spending a single vault utxo, satisfying
<uN> <delay> <rN> OP_VAULT
via the uN path, creating an output of
<outhash> <delay> <rN> OP_UNVAULT
* 1 tx spending all the OP_UNVAULT outputs to a common set of outputs
<uN>, with nSequence set to a relative timelock of at least <delay>
Whereas if you use an identical OP_VAULT script for all the utxos in your vault, that can look like:
* 1 tx, spending all the vault utxos, to a single OP_UNVAULT output,
with the same <delay> <rN> that all the inputs share.
* 1 tx spending the OP_UNVAULT output after a delay
But maybe you can get the best of both worlds just by having the unvault path for OP_VAULT require you to put the vout number for its corresponding OP_UNVAULT output on the stack? Then if you're doing address reuse, you use a single vout for multiple inputs; and if you're avoiding address reuse, you use multiple outputs, and provide the mapping between inputs and outputs explicitly.
Cheers, aj
I like the proposal of a targeted wallet vault opcode. It keeps things constrained, limiting objections to those of the form "but if we had X it would do all this and more so why add this complexity when it will be obsoleted in the future?"
Quoted text (2 lines)
> An idealized vault > no existing vault design meets this set of features.
My proposal for efficient wallet vaults <https://github.com/fresheneesz/bip-efficient-bitcoin-vaults> was designed to meet all of those criteria, and allows batching as well. It also allows sending in a single transaction vs the two it would take with OP_VAULT and several other benefits. However, it uses a general covenant opcode to do it, along with several other new opcodes specified in that write up.
Quoted text (1 line)
> it must first be spent into an OP_UNVAULT output
I don't see in the write up how a node verifies that the destination of a spend using an OP_VAULT output uses an appropriate OP_UNVAULT script. I see you mentioned above (but not in the write up) that the script pub key needs to be bare. But it would be very helpful if you detailed exactly how this is intended to be done in that document.
It seems that Greg Sanders noticed the same thing. I like his suggestion as you reworded it above, makes a lot of sense.
Quoted text (1 line)
> I think the fix is just requiring a single additional witness data item
during OP_VAULT spend (for unvault path), mandating the <target-outputs-hash> to be included in the witness stack as an input to OP_VAULT opcode, and transaction introspection then checks to make sure the witness item and the corresponding output script template matches the expected.
Quoted text (1 line)
> If it becomes necessary to make use of the recovery path, the recovery
scriptPubKey will be revealed, which means that any other vaults with that recovery path may be swept there by an unauthenticated party.
Another issue is that the recovery path becomes the easiest mechanism of attack. It would usually be prudent to store this recovery address with every key to the vault, and potentially in other places as well, so as to minimize the possibility that the funds are lost or stolen. In such a situation, this means that an attacker that finds any key can grief the vault by spending it to its recovery address. My "efficient wallet vault" design I mentioned above doesn't have this issue, nor the associated batching DoS attack.
Quoted text (2 lines)
> if the recovery path should be committed with a signature > This would enable a "sign-to-recover" flow at the option of the user,
specified during vault creation.
This is rather limiting isn't it? Losing the key required to sign loses your recovery option. Seems brittle. It seems better to me to include a <recovery-initiation-spk-hash> that operates similarly to <recovery-spk-hash> - where some arbitrary script must be fulfilled to allow the recovery path to be spent. For a recovery path, you'd probably often want one of the keys required to spend from the recovery address, since without access to one of those keys, you can't spend from the recovery address anyway (and the spend path is an effective burn). Having some ability to allow an n-of-m situation for triggering the recovery path seems desirable.
Quoted text (1 line)
> What do you think about the idea of making the recovery-path
authorization behavior variable on a single byte flag preceding the 32 byte data push, as I mentioned in another post?
A more arbitrary construct here that allows you to use any kind of recovery script would be much more flexible and preclude the need for any kind of switching like this. It seems like it would also solve the issue Andrew Chow mentioned where recovery transactions can only be batched if they all share the same recovery output, since each output can simply include the appropriate witness matching its recovery scriptPubkey.
Tho I don't think I quite understand why you mention the constraint requiring recovery batching to only be done with vault outputs that have matching recovery destinations. Wouldn't it be reasonably possible to allow recovery outputs with any recovery address to be batched, and the amount sums sent to each to be added up and verified?
Quoted text (1 line)
> 1. script validation rules could require some allowable “range” of
amount discounts
Quoted text (2 lines)
> seems like a bad design > 2. script validation rules could require that the unvault/recovery
outputs preserve the full value
Quoted text (1 line)
> seems like the preferable approach
Both have tradeoffs. I would not call #1 an inherently bad design. I would point out that for 2, disallowing the spending of vault funds without access to already-unvaulted bitcoin seems like a very inconvenient design, since it would require you in the best of cases to create more complex transactions (or cpfp transaction chains) involving a 2nd wallet that you involve in the unvaulting process, and in the worst case (if you have no other bitcoin or other money on hand) you have to go asking a 3rd party for their bitcoin to use in the unvaulting process. If someday wallet vaults are the standard wallet construct, people might not even want to have a non-vault wallet just for use in unvaulting.
#1 is the approach I used to design OP_LIMITFEECONTRIBUTION <https://github.com/fresheneesz/bip-efficient-bitcoin-vaults/blob/main/lfc/bip-limit-fee-contribution.md>, which allows for a fee-range specification that depends on recent median fees included in blocks. This allows rather flexibly limiting fees to a particular range of "priorities" regardless of fee environment.
For #2, it seems like Jeremy Rubin's Sponsor transactions would be ideal for facilitation of adding a fee to an unvaulting transaction.
Quoted text (1 line)
> In the case of a withdrawal, unvaulted funds can skip the “warm” wallet
step that precomputed vault funds must pass through on their way to destinations only known at unvault time.
Hmm, it seems inaccurate to say that step is "skipped". While there isn't a warm wallet step, its replaced with an OP_UNVAULT script step. So its not skipped as much as modified I think, right?
It looks like the way the OP_UNVAULT is specified prevents any use where you don't know the full set of outputs, which might happen in cases where certain sighash flags might be useful (signing some outputs you know, but allowing outputs that you don't know to be added later). This is another thing my "efficient wallet vault" design should allow.
On Tue, Jan 17, 2023 at 1:47 AM Anthony Towns via bitcoin-dev < bitcoin-dev@lists•linuxfoundation•org> wrote:
Quoted text (64 lines)
> On Mon, Jan 16, 2023 at 11:47:09PM +0000, Andrew Chow via bitcoin-dev > wrote: > > It seems like this proposal will encourage address reuse for vaults, > > (That is listed as an explicit goal: "A single vault scriptPubKey should > be able to "receive" multiple deposits") > > > However the current construction makes it impossible to spend these > > vaults together. Since OP_VAULT requires the recovery script of the > > unvault output to match what's provided in the input, > > I don't think this part is a big problem -- the recovery path requires > revealing a secret, but if you separate that secret from the recovery > path sPK, you could vary the secret. ie: > > unvault1 delay recovery1 VAULT > unvault2 delay recovery2 VAULT > > where recovery1 = SHA256(SHA256(secret1), rSPK) and recovery2 = > SHA256(SHA256(secret2), rSPK), and both are spendable when the top stack > element is secretN and the first output pays at least the sum of all > the OP_VAULT using inputs to rSPK. So batched recovery could work fine, > I think. > > (If you're using the same "recovery" parameter to each VAULT, then > you're revealing which txs are in your vault at spend time, rather than > at receive time, which doesn't seem all that much better to me) > > But the problem with this is it prevents you from combining vaults when > spending normally: so if you've got a bunch of vaults with 1 BTC each, > and want to spend 10 BTC on a house, you'll need to make 11 separate > transactions: > > * 10 txs each spending a single vault utxo, satisfying > <uN> <delay> <rN> OP_VAULT > via the uN path, creating an output of > <outhash> <delay> <rN> OP_UNVAULT > > * 1 tx spending all the OP_UNVAULT outputs to a common set of outputs > <uN>, with nSequence set to a relative timelock of at least <delay> > > Whereas if you use an identical OP_VAULT script for all the utxos in > your vault, that can look like: > > * 1 tx, spending all the vault utxos, to a single OP_UNVAULT output, > with the same <delay> <rN> that all the inputs share. > > * 1 tx spending the OP_UNVAULT output after a delay > > But maybe you can get the best of both worlds just by having the unvault > path for OP_VAULT require you to put the vout number for its corresponding > OP_UNVAULT output on the stack? Then if you're doing address reuse, you > use a single vout for multiple inputs; and if you're avoiding address > reuse, you use multiple outputs, and provide the mapping between inputs > and outputs explicitly. > > Cheers, > aj > > _______________________________________________ > bitcoin-dev mailing list > bitcoin-dev@lists•linuxfoundation•org > https://lists.linuxfoundation.org/mailman/listinfo/bitcoin-dev >
I've implemented three changes based on suggestions from Greg Sanders and AJ Towns.
I've segmented the changes into commits that should be reasonable to follow, even though I'll probably rearrange the commit structure later on.
1. Greg's suggestion: OP_UNVAULT outputs can now live behind scripthashes. This means that the lifecycle of a vault can live entirely within, say, Taproot. In this case the only thing that would reveal the operation of the vault would be the content of the witness stack when triggering an unvault or recovering. So I think no real privacy benefits over the previous scheme, but certainly some efficiency ones since we're moving more content from the scriptPubKey into the witness.
Commit here: https://github.com/bitcoin/bitcoin/pull/26857/commits/cd33d120c67cda7eb5c6bbfe3a1ea9fb6c5b93d1
2. AJ's suggestion: unvault trigger transactions can now have an extra "revault" output that redeposits some balance to the same vault scriptPubKey from which it came. This is nice because if the delay period is long, you may want to manage a remaining vault balance separately while the spent balance is pending an unvault.
Commit here: https://github.com/bitcoin/bitcoin/pull/26857/commits/cf3764fb503bc17c4438d1322ecf780b9dc3ef30
3. AJ's suggestion: instead of specifying <recovery-spk-hash>, introduce a replacement parameter: <recovery-params>. This contains the same target recovery sPK hash as before, but the remaining bytes contain a scriptPubKey that functions as authorization for the recovery process. This allows users to optionally avoid the risk of "recovery replays" at the expense of having to maintain a recovery key. Users can opt out of this by passing OP_TRUE for the recovery sPK. I guess maybe I could even support just omitting an sPK altogether for the legacy behavior.
Commit here: https://github.com/bitcoin/bitcoin/pull/26857/commits/fdfd5e93f96856fbb41243441177a40ebbac6085
The suggestions were good ones, and I think they've improved the proposal.
My next steps are to do minor updates to the paper and start writing a BIP draft.
Thanks to achow for the valuable feedback, which I'm still mulling on.
James
Quoted text (3 lines)
> I don't see in the write up how a node verifies that the destination > of a spend using an OP_VAULT output uses an appropriate OP_UNVAULT > script.
It's probably quicker for you to just read through the implementation that I reference in the last section of the paper.
Quoted text (2 lines)
> It would usually be prudent to store this recovery address with every > key to the vault, ...
I'm not sure I really follow here. Worth noting now that in OP_VAULT the recovery path can be optionally gated by an arbitrary scriptPubKey.
Quoted text (2 lines)
> This is rather limiting isn't it? Losing the key required to sign > loses your recovery option.
This functionality is optional in OP_VAULT as of today. You can specify OP_TRUE (or maybe I should allow empty?) in the <recovery-params> to disable any signing necessary for recovery.
Quoted text (3 lines)
> Wouldn't it be reasonably possible to allow recovery outputs with any > recovery address to be batched, and the amount sums sent to each to be > added up and verified?
I think the space savings from this is pretty negligible, since you're just saving on the transaction overhead, and it makes the implementation decently more complicated. One benefit might be sharing a common fee-management output (e.g. ephemeral anchor) across the separate vaults being recovered.
Quoted text (3 lines)
> If someday wallet vaults are the standard wallet construct, people > might not even want to have a non-vault wallet just for use in > unvaulting.
If you truly lacked any non-vaulted UTXOs and couldn't get any at a reasonable price (?), I can imagine there might be a mechanism where you include a payout output to some third party in a drafted unvault trigger transaction, and they provide a spend of the ephemeral output.
Though you do raise a good point that this construction as written may not be compatible with SIGHASH_GROUP... I'd have to think about that one.
Quoted text (2 lines)
> Hmm, it seems inaccurate to say that step is "skipped". While there > isn't a warm wallet step, its replaced with an OP_UNVAULT script step.
It is "skipped" in the sense that your bitcoin can't be stolen by having to pass through some intermediate wallet during an authorized withdrawal to a given target, in the way that they could if you had to prespecify an unvault target when creating the vault.
---
Quoted text (2 lines)
> My proposal for efficient wallet vaults was designed to meet all of > those criteria, and allows batching as well.
Probably a discussion of your proposal merits a different thread, but some thoughts that occur:
Quoted text (5 lines)
> [from the README] > > OP_BEFOREBLOCKVERIFY - Verifies that the block the transaction is > within has a block height below a particular number. This allows a > spend-path to expire.
I think this breaks fundamental reorgability of transactions. I think some of the other opcodes, e.g the one that limits fee contribution on the basis of historical feerate, are going to be similarly controversial.
Quoted text (2 lines)
> This is done by using a static intermediate address that has no values > that are unique to the particular wallet vault address.
Does mean either that (i) this proposal doesn't have dynamic unvaulting targets or, (ii) if you do, in order to be batch unvaulted, vaulted coins need to first be spent into this intermediate output?
It sounds like (ii) is the case, given that your unvault target specification lives in (I think?) the witness for the spend creating the intermediate output.
If the intermediate address doesn't have any values which are unique to a particular vault, how do you authorize recoveries from it?
---
Personally I think if you'd like to pursue your proposal, it'd be valuable to see a full implementation. Might also make it easier to assess the viability of the proposal.
Quoted text (1 line)
>> It would usually be prudent to store this recovery address with every key
to the vault, ...
Quoted text (1 line)
> Worth noting now that in OP_VAULT the recovery path can be optionally
gated by an arbitrary scriptPubKey.
Gating by a scriptPubKey solves the problem I was talking about there. However, thinking about it more, I realized doing this basically turns OP_VAULT into something able to do general covenants. By making `unvault-target-hash` unsatisfiable (set to some random number that isn't derived from a hash) the delay wouldn't matter, but arbitrary conditions can be set on spending the utxo to the "recovery address" which could be another OP_UNVAULT destination. It seems like that could be used as a general CTV-like covenant.
Quoted text (4 lines)
>> Wouldn't it be reasonably possible to allow recovery outputs with any >> recovery address to be batched, and the amount sums sent to each to be >> added up and verified? > I think the space savings from this is pretty negligible
Besides space savings, there's the consideration of the usability of the vault and downstream code complexity. One of the criteria I designed the "efficient wallet vaults" opcodes for is that the vault spend should be spendable in situations where a normal output is spendable as much as possible. Having a constraint that prevents one type of otherwise spendable output from being combined with another type would add complexity to all downstream code which now has to have special cases - either a simple error and recovery if they just want to disallow that type of opcode being combined with other types (which may degrade the user experience by asking them to provide a different utxo or do an unvaulting first), or some kind of special case handling to make it work. Any kind of hybrid wallet incorporating a vault (eg a wallet that combines a vault and a hot address or lightning channel, kind of like Phoenix combines a lightning channel and a normal onchain wallet) would need to deal with this kind of extra complexity during utxo selection.
Are there currently any situations where one otherwise-spendable utxo can't be mixed with another? If not, this added edge case deserves some extra consideration I think.
Quoted text (1 line)
> I can imagine there might be a mechanism where you include a payout
output to some third party in a drafted unvault trigger transaction, and they provide a spend of the ephemeral output.
I agree that's doable. I just think it merits some consideration as to whether that complexity (both for downstream code and for users) is a favorable trade off vs having a solution to reasonably bound fees spendable from the vault.
Consider the case where a self-custodying user would have a small set of keys (2? 3?) and use all those keys to secure their vault, and just 1 of them to secure their hot wallet. It doesn't seem an implausible case and I could imagine that kind of set up becoming quite common. In such a case, if the hot wallet key is stolen, it means one vault key is also stolen and the hot wallets funds could be stolen at the same time as an unvaulting is triggered. The need to figure out how to coordinate a 3rd party's help to recover is at best an added difficulty and delay.
An alternative would be to keep a completely separate hot wallet key that isn't use as part of the vault. But because key storage is by far the most difficult and costly part of self-custody, every additional key that needs to be stored is a significant additional burden (that's one of the benefits of wallet vaults - fewer seeds needed for a given amount of security/redundancy).
Another alternative would be to have a hot wallet that for its primary spend-path uses a memory-only passphrase on one of the vault seeds (so compromise of a vault seed won't compromise the hot wallet) and has a recovery spend path that uses multiple (or all) vault seeds to recover if you forget the passphrase. It certainly seems like something can be worked out here to make the end user experience reasonable, but the additional operational complexity this would entail still deserves consideration.
Quoted text (2 lines)
>> OP_BEFOREBLOCKVERIFY > I think this breaks fundamental reorgability of transactions.
I discuss this in the Reorg Safety section here <https://github.com/fresheneesz/bip-efficient-bitcoin-vaults/blob/main/bbv/bip-beforeblockverify.md#reorg-safety> .
Quoted text (3 lines)
>> This is done by using a static intermediate address that has no values >> that are unique to the particular wallet vault address. > Does mean .. that (ii) .. in order to be batch unvaulted [with dynamic
unvaulting targets], vaulted
Quoted text (1 line)
> coins need to first be spent into this intermediate output?
It does support dynamic unvaulting using OP_PUSHOUTPUTSTACK <https://github.com/fresheneesz/bip-efficient-bitcoin-vaults/blob/main/pos/bip-pushoutputstack.md>, which adds data to an output that carries over to the execution when spending the output created by a utxo that uses OP_PUSHOUTPUTSTACK. So the design is done such that once the intermediate output has been confirmed and the unvaulting delay has passed, it is then fully owned by the recipient without a second transaction (because of the use of OP_BEFOREBLOCKVERIFY). If OP_BEFOREBLOCKVERIFY is deemed to be unacceptable, then the intermediate output is fully intermediate and a 2nd transaction would be required to get it to its committed recipient.
Quoted text (1 line)
> it'd be valuable to see a full implementation
While OP_BEFOREBLOCKVERIFY can be dropped with only somewhat minor degraded usability, OP_PUSHOUTPUTSTACK is necessary for the proposal to work as intended. I would want to see some support for the high-level concepts it introduces before spending significant time on an implementation. It does something fundamentally new that other opcodes haven't done: add "hidden" data onto the output that allows for committing to destination addresses. Maybe something along the line of Greg Sanders' suggestion for your proposal could replace the need for this, but I'm not sure its possible with how OP_CD is designed.
On Wed, Jan 18, 2023 at 5:38 PM James O'Beirne <james.obeirne@gmail•com> wrote:
Quoted text (96 lines)
> > I don't see in the write up how a node verifies that the destination > > of a spend using an OP_VAULT output uses an appropriate OP_UNVAULT > > script. > > It's probably quicker for you to just read through the > implementation that I reference in the last section of the paper. > > > https://github.com/bitcoin/bitcoin/blob/fdfd5e93f96856fbb41243441177a40ebbac6085/src/script/interpreter.cpp#L1419-L1456 > > > It would usually be prudent to store this recovery address with every > > key to the vault, ... > > I'm not sure I really follow here. Worth noting now that in OP_VAULT the > recovery path can be optionally gated by an arbitrary scriptPubKey. > > > This is rather limiting isn't it? Losing the key required to sign > > loses your recovery option. > > This functionality is optional in OP_VAULT as of today. You can specify > OP_TRUE (or maybe I should allow empty?) in the <recovery-params> to > disable any signing necessary for recovery. > > > Wouldn't it be reasonably possible to allow recovery outputs with any > > recovery address to be batched, and the amount sums sent to each to be > > added up and verified? > > I think the space savings from this is pretty negligible, since you're > just saving on the transaction overhead, and it makes the implementation > decently more complicated. One benefit might be sharing a common > fee-management output (e.g. ephemeral anchor) across the separate vaults > being recovered. > > > If someday wallet vaults are the standard wallet construct, people > > might not even want to have a non-vault wallet just for use in > > unvaulting. > > If you truly lacked any non-vaulted UTXOs and couldn't get any at a > reasonable price (?), I can imagine there might be a mechanism where you > include a payout output to some third party in a drafted unvault trigger > transaction, and they provide a spend of the ephemeral output. > > Though you do raise a good point that this construction as written may > not be compatible with SIGHASH_GROUP... I'd have to think about that > one. > > > Hmm, it seems inaccurate to say that step is "skipped". While there > > isn't a warm wallet step, its replaced with an OP_UNVAULT script step. > > It is "skipped" in the sense that your bitcoin can't be stolen by having > to pass through some intermediate wallet during an authorized withdrawal > to a given target, in the way that they could if you had to prespecify > an unvault target when creating the vault. > > > --- > > > > My proposal for efficient wallet vaults was designed to meet all of > > those criteria, and allows batching as well. > > Probably a discussion of your proposal merits a different thread, but > some thoughts that occur: > > > > [from the README] > > > > OP_BEFOREBLOCKVERIFY - Verifies that the block the transaction is > > within has a block height below a particular number. This allows a > > spend-path to expire. > > I think this breaks fundamental reorgability of transactions. I think > some of the other opcodes, e.g the one that limits fee contribution on > the basis of historical feerate, are going to be similarly > controversial. > > > This is done by using a static intermediate address that has no values > > that are unique to the particular wallet vault address. > > Does mean either that (i) this proposal doesn't have dynamic unvaulting > targets or, (ii) if you do, in order to be batch unvaulted, vaulted > coins need to first be spent into this intermediate output? > > It sounds like (ii) is the case, given that your unvault target > specification lives in (I think?) the witness for the spend creating the > intermediate output. > > If the intermediate address doesn't have any values which are unique to > a particular vault, how do you authorize recoveries from it? > > --- > > Personally I think if you'd like to pursue your proposal, it'd be > valuable to see a full implementation. Might also make it easier to > assess the viability of the proposal. >
Andrew, thanks for taking the time.
Quoted text (4 lines)
> It seems like this proposal will encourage address reuse for vaults, > at least in some parts. It seems like it would not be difficult to > ensure that each vault address was unique through the use of key > derivation.
I think it's worth stepping back and noting that this proposal defers the level of privacy-vs.-efficiency to be decided by the end user.
For users who are very privacy conscious and are doing fairly low volume (a few vaulted coins a month?), trading the ability to do batched operations for no privacy loss seems reasonable. They can use a ranged descriptor to generate recovery paths and unvault sPKs and reveal no marginal information during recoveries or unvaults.
Though of course there still may be an obvious temporal association across transactions in the case of recovery - everything with the same unvault key has to be recovered at once.
For users who expect to have large numbers of vaulted coins and maybe don't care as much about privacy (e.g. many commercial users), revealing the related nature of coins that are being unvaulted or recovered seems like an okay cost to pay. Such users might decide to create "tranches" of vaults with different parameters in whatever manner makes sense for their use.
Importantly: in either case, you can always keep the nature of still-vaulted coins hidden by burying the OP_VAULT script in a taptree and varying the internal key you use for each coin with a ranged descriptor. This way, only the revealed contents of unvaults and recoveries can be associated. So I think that's your worst case, which really doesn't seem bad to me.
As an aside, a goal in supporting address reuse wasn't for address reuse *per se* - it was to remove potential catastrophe resulting from the case where you give someone a vault address to deposit to, and they wind up depositing to it multiple times - whether you warned them against it or not.
Quoted text (2 lines)
> I'm not sure how [batching without privacy loss] could be solved > though.
For recovery, I think it might be intractable at the moment.
Seemingly unrelated vaults which have the same recovery parameters will presumably be recovered together if the unvault key is compromised. The simple fact that these outputs are being spent together and are OP_VAULT scripts fundamentally reveals their association during recovery, no way around that AFAICT.
Similarly for the unvaulting, you can't get around revealing that you're spending a group of outputs that contain an OP_VAULT script.
As mentioned above, unvaulting -- regardless of whether your vault configuration supports batching or not -- *never* has to correlate unvaulted coins to still-vaulted coins if you are either
(i) varying the internal key used for the OP_VAULT taptrees, or
(ii) using the optional "authorized recovery" feature and are varying
that sPK with a ranged descriptor.
So there's really never a case where unvaults have to reveal a relationship to, or between, still-vaulted coins. Subsequent unvaults may be able to be correlated though on the basis of the recovery sPK target hash.
Quoted text (2 lines)
> It just means that the recovery scripts must be the same, and this > would leave an identifying mark on chain for every unvault.
This is only true if the user decides to create vaults which share "literal" recovery paths. At the risk of belaboring the point for clarity, you can avoid this by generating the different "literal" recovery paths from a ranged descriptor which is controlled by a single private key -- of course at the cost of no batched recovery.
Quoted text (2 lines)
> not to mention that sweeping to recovery would also reveal all of your > coins too.
Maybe it's worth contextualizing that recovery sweeps really only happen as a final fallback in catastrophic cases that, in a world without vaults, would result in the coins simply being stolen. In this case I would guess most users are willing to make the privacy trade to retain ownership of their coins.
Quoted text (4 lines)
> I think it would be sufficient to do the same check as the OP_UNVAULT > script [when validating the revault output in the unvault trigger > transaction] and just require that the recovery script and the delay > are the same
Consider that this allows the holder of the unvault key (possibly an attacker) to immediately spend the vault into a new vault configuration with a different unvault key.
Obviously the recovery path would still be the same, and so the owner of the vault could still sweep if the unvault key switch was unauthorized, but I'll need to think a little bit more about whether this change is more fundamental.
Generally that would be easy to implement, though. I'll think on it. My suspicion is that you're right and this would be a good change.
Quoted text (4 lines)
> I'm also not convinced that OP_VAULT and OP_UNVAULT should be allowed > for bare and P2WSH outputs. It seems like it would make sense to just > limit their usage to tapscripts as this would simply their > implementation.
I think you're right, and accordingly I'll shortly be reimplementing this with OP_SUCCESSx overrides instead of OP_NOPs. I'd be curious to know if anyone has any objections to this - i.e. if there's a solid case for vaults in a pre-taproot context.
James