AJ,

Interesting stuff! Just a couple thoughts on these proposed opcodes, at least one we discussed elsewhere:

1) OP_FORWARD_SELF is a JET of OP_FLU in the revaulting common case. Maybe obvious but I missed this initially and thought it was useful to be pointed out.

2)  Using these extended primitives, you can do rate-limiting of the two step unvaulting, or just a single step vault by committing to the partial values. For the single stage case it's something like:
 
$recovery = Same As Before

$withdrawal = <deposit-delay> OP_CSV OP_DROP <pubkey> OP_CHECKSIG OP_DUP <V> OP_LESSTHANOREQUAL OP_VERIFY OP_FORWARD_PARTIAL OP_FORWARD_TARGET OP_FORWARD_SELF

$withdrawal is spent by:

<self-idx> <target-idx> <spk> <0<=v<=V> <signature>

where "V"  is the max allowed withdrawal value, and "deposit-delay" the required gap in withdrawals

Due to the OP_LEQ, you are bound to ~21 BTC in value for this operation, but for larger vaults it's pretty trivial to add larder fixed denominations to "peel out" value until you get to your final ~21 BTC.

This rate-limiting(in either the two-stage or one-stage scheme) can limit the risk of theft during a watchtower outage to a constant value per utxo per time period of watchtower failure. As we've seen in the past with LN infrastructure, software risks are often correlated, so it's a good idea to build in belt and suspenders where we can or at least have them available when possible.

Cheers,
Greg


On Tue, Mar 7, 2023 at 7:45 AM Anthony Towns <aj@erisian.com.au> wrote:
On Mon, Mar 06, 2023 at 10:25:38AM -0500, James O'Beirne via bitcoin-dev wrote:
> What Greg is proposing above is to in essence TLUV-ify this proposal.

FWIW, the way I'm thinking about this is that the "OP_VAULT" concept is
introducing two things:

 a) the concept of "forwarding" the input amount to specified
    outputs in a way that elegantly allows merging/splitting

 b) various restrictions on the form of the output scripts

These concepts go together well, because restricting an output script is
only an interesting thing to do if you're moving value from this input
into it. And then it's just a matter of figuring out a nice way to pick
opcodes that combine those two concepts in interesting ways.

This is different from TLUV, in that TLUV only did part (b), and
assumed you'd do part (a) manually somehow, eg via "OP_IN_OUT_AMOUNT"
and arithmetic opcodes. The advantage of this new approach over that
one is that it makes it really easy to get the logic right (I often
forgot to include the IN_OUT_AMOUNT checks at all, for instance), and
also makes spending multiple inputs to a single output really simple,
something that would otherwise require kind-of gnarly logic.

I think there are perhaps four opcodes that are interesting in this class:

   idx sPK OP_FORWARD_TARGET
     -- sends the value to a particular output (given by idx), and
        requires that output have a particular scriptPubKey (given
        by sPK).

   idx [...] n script OP_FORWARD_LEAF_UPDATE
     -- sends the value to a particular output (given by idx), and
        requires that output to have almost the same scriptPubKey as this
        input, _except_ that the current leaf is replaced by "script",
        with that script prefixed by "n" pushes (of values given by [...])

   idx OP_FORWARD_SELF
     -- sends the value to a particular output (given by idx), and
        requires that output to have the same scriptPubKey as this input

   amt OP_FORWARD_PARTIAL
     -- modifies the next OP_FORWARD_* opcode to only affect "amt",
        rather than the entire balance. opcodes after that affect the
        remaining balance, after "amt" has been subtracted. if "amt" is
        0, the next OP_FORWARD_* becomes a no-op.

Then each time you see OP_FORWARD_TARGET or OP_FORWARD_LEAF_UPDATE, you
accumulate the value that's expected to be forwarded to the output by
each input, and verify that the amount for that output is greater-or-equal
to the accumulated value.

> ## Required opcodes
> - OP_VAULT: spent to trigger withdrawal
> - OP_VAULT_RECOVER: spent to recover

Naming here is OP_VAULT ~= OP_FORWARD_LEAF_UPDATE; OP_VAULT_RECOVER ~=
OP_FORWARD_TARGET.

> For each vault, vaulted coins are spent to an output with the taproot
> structure
>
>   taproot(internal_key, {$recovery_leaf, $trigger_leaf, ...})
>
> where
>
>   $trigger_leaf =
>     <trigger> <auth> <script> <spend-delay> OP_VAULT

With the opcodes above, the trigger_leaf (for spending some of the
vaulted funds via your hot wallet) script might look like:

   OP_FORWARD_PARTIAL OP_FORWARD_SELF
   1 "288 OP_CSV OP_DROP OP_CTV" OP_FORWARD_LEAF_UPDATE
   key CHECKSIG

So if you have 2.0 BTC in a vault utxo, you might spend 0.4 BTC by
supplying the witness data:

  160000000  (1.6BTC in sats)
  0 (output 0 puts 1.6BTC goes back into the vault)
  <ctvhash> (the outputs where you want the remaining 0.4 BTC to end up)
  1 (the output idx that will be spend via the CTV path once the CSV
     delay is done)
  <sig> (a signature of this transaction via the hot wallet "key")

That is, the script becomes:

  160000000 FORWARD_PARTIAL
  0 FORWARD_SELF
  1 <ctvhash> 1 "288 CSV DROP CTV" FORWARD_LEAF_UPDATE
  sig key CHECKSIG

Output 1 would then have a tapscript of "<ctvhash> 288 OP_CSV OP_DROP
OP_CTV", satisfied with an empty witness stack (along with the recovery
path, etc).

Output 0 is just 1.6BTC back in your vault, and immediately available
for use.

Other inputs/outputs (for fees etc) would still be committed to by <sig>,
so nothing here is malleable. The script here is about 45 bytes (compared
to 34 for a simple "key CHECKSIG") and the witness data is about 105 bytes
(compared to 65 bytes for just a signature), which seems pretty nice.

>   ... =
>     other (optional) leaves in the taptree

This would allow you to have multiple hot wallets (if any of them are
compromised you can still use the recovery path to avoid loss of funds;
but if some hot wallet becomes temporarily *inaccessible* you can still
easily spend the funds via one of the alternative hot wallets), or,
if you have multiple watchtowers validating your spends and recovering
funds to your cold wallet on a violation, you could have multiple recovery
paths to provide some auditability for who triggered the recovery.

> Happens via script-path spend to $expr_withdraw, i.e. a timelocked
> OP_CTV.

Note that if you calculated the OP_CTV incorrectly (eg, you don't set a
correct nSequence timelock, so that any tx that passes OP_CTV won't pass
the OP_CSV check, and vice-versa) then this spend path becomes invalid,
and the funds can only be reclaimed via some other path (key path spend,
recovery tapscript, potentially an alternative hotwallet script path).

OP_FORWARD_LEAF_UPDATE is equivalent to a very specific form of TLUV,
namely "FALSE <h> 2 TLUV", where "<h>" is calculated by building the
script, prefixing the pushes, then doing the Hash_TapLeaf calculation.

Not being able to tweak the internal public key ("FALSE" rather than
"<x>") means this can't be used to build a coinpool with unilateral
exit -- you can't remove your key from the key path, which screws over
everyone who's still in the coinpool.

On the other hand, not tweaking the internal public key avoids introducing
all the x-only pubkey complications, and keeps it relatively simple,
which is nice, and keeping things simple and targeted now means there's
still plenty of OP_SUCCESS opcodes available later for something more
general, if that turns out to be desirable.

Cheers,
aj