0345 - Withdrawn - Standards - Feb. 3, 2023 (3 years, 5 months ago)
3
2023
BIP: 345
Layer: Consensus (soft fork)
Title: OP_VAULT
Author: James O'Beirne <[email protected]>
Greg Sanders <[email protected]>
Comments-URI: https://github.com/bitcoin/bips/wiki/Comments:BIP 345
Status: Withdrawn
Type: Standards Track
Created: 2023-02-03
License: BSD-3-Clause
Post-History: 2023-01-09: https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2023-January/021318.html [bitcoin-dev] OP_VAULT announcement
2023-03-01: https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2023-March/021510.html [bitcoin-dev] BIP for OP_VAULT
Superseded-By: 443
This BIP proposes two new tapscript opcodes that add consensus
support for a specialized covenant:
OP_VAULT
and
OP_VAULT_RECOVER
. These opcodes, in conjunction with
OP_CHECKTEMPLATEVERIFY
(
BIP
119
), allow users to enforce a delay period before designated coins
may be spent to an arbitrary destination, with the exception of a
prespecified "recovery" path. At any time prior to final withdrawal, the
coins can be spent to the recovery path.
This document is licensed under the 3-clause BSD license.
The hazard of custodying Bitcoin is well-known. Users of Bitcoin must go to significant effort to secure their private keys, and hope that once provisioned their custody system does not yield to any number of evolving and persistent threats. Users have little means to intervene once a compromise is detected. This proposal introduces a mechanism that significantly mitigates the worst-case outcome of key compromise: coin loss.
Introducing a way to intervene during unexpected spends allows users to incorporate highly secure key storage methods or unusual fallback strategies that are only exercised in the worst case, and which may otherwise be operationally prohibitive. The goal of this proposal is to make this strategy usable for custodians of any size with minimal complication.
A common configuration for an individual custodying Bitcoin is "single signature and passphrase" using a hardware wallet. A user with such a configuration might be concerned about the risk associated with relying on a single manufacturer for key management, as well as physical access to the hardware.
This individual can use
OP_VAULT
to make use of a highly
secure key as the unlikely recovery path, while using their existing
signing procedure as the withdrawal trigger key with a configured spend
delay of e.g. 1 day.
The recovery path key can be of a highly secure nature that might otherwise make it impractical for daily use. For example, the key could be generated in some analog fashion, or on an old computer that is then destroyed, with the private key replicated only in paper form. Or the key could be a 2-of-3 multisig using devices from different manufacturers. Perhaps the key is geographically or socially distributed.
Since it can be any Bitcoin script policy, the recovery key can include a number of spending conditions, e.g. a time-delayed fallback to an "easier" recovery method, in case the highly secure key winds up being too highly secure.
The user can run software on their mobile device that monitors the blockchain for spends of the vault outpoints. If the vaulted coins move in an unexpected way, the user can immediately sweep them to the recovery path, but spending the coins on a daily basis works in the same way it did prior to vaulting (aside from the spend delay).
Institutional custodians of Bitcoin may use vaults in similar fashion.
This proposal provides a mitigation to the "$5 wrench attack." By setting the spend delay to, say, a week, and using as the recovery path a script that enforces a longer relative timelock, the owner of the vault can prove that he is unable to access its value immediately. To the author's knowledge, this is the only way to configure this defense without rolling timelocked coins for perpetuity or relying on a trusted third party.
Vaults in Bitcoin have been discussed formally since 2016 ( MES16 ) and informally since 2014 . The value of having a configurable delay period with recovery capability in light of an unexpected spend has been widely recognized.
The only way to implement vaults given the existing consensus rules, aside from emulating vaults with large multisig configurations , is to use presigned transactions created with a one-time-use key. This approach was first demonstrated in 2020 .
Unfortunately, this approach has a number of practical shortcomings:
The deployment of a "precomputed" covenant mechanism like OP_CHECKTEMPLATEVERIFY or SIGHASH_ANYPREVOUT would both remove the necessity to use an ephemeral key, since the covenant is enforced on-chain, and lessen the burden of sensitive data storage, since the necessary transactions can be generated from a set of compact parameters. This approach was demonstrated in 2022 .
However, the limitations of precomputation still apply: amounts, destinations, and fee management are all fixed. Funds must flow through a fixed intermediary to their final destination. Batch operations, which may be vital for successful recovery during fee spikes or short spend delay, are not possible.
Having a "general" covenant mechanism that can encode arbitrary transactional state machines would allow us to solve these issues, but at the cost of complex and large scripts that would probably be duplicated many times over in the blockchain. The particular design and deployment timeline of such a general framework is also uncertain. This approach was demonstrated in 2016 .
This proposal intends to address the problems outlined above by providing a delay period/recovery path use with minimal transactional and operational overhead using a specialized covenant.
The design goals of the proposal are:
scriptPubKey
or not, should be
able to “receive” multiple deposits.
These goals are accompanied by basic safety considerations (e.g. not being vulnerable to mempool pinning) and a desire for concision, both in terms of the number of outputs created as well as script sizes.
This proposal is designed to be compatible with any future sighash
modes (e.g.
SIGHASH_GROUP
) or fee management strategies
(e.g.
transaction
sponsors
) that may be introduced. Use of these opcodes will benefit
from, but do not strictly rely on,
v3
transaction relay
and
ephemeral
anchors
.
In typical usage, a vault is created by encumbering coins under a
taptree
(BIP
341)
containing at least two leaves: one with an
OP_VAULT
-containing script that facilitates the expected
withdrawal process, and another leaf with
OP_VAULT_RECOVER
which ensures the coins can be recovered at any time prior to withdrawal
finalization.
The rules of
OP_VAULT
ensure the timelocked,
interruptible withdrawal by allowing a spending transaction to replace
the
OP_VAULT
tapleaf with a prespecified script template,
allowing for some parameters to be set at spend (trigger) time. All
other leaves in the taptree must be unchanged in the destination output,
which preserves the recovery path as well as any other spending
conditions originally included in the vault. This is similar to the
TAPLEAF_UPDATE_VERIFY
design that was proposed
in
2021
.
These tapleaf replacement rules, described more precisely below,
ensure a timelocked withdrawal, where the timelock is fixed by the
original
OP_VAULT
parameters, to a fixed set of outputs
(via
OP_CHECKTEMPLATEVERIFY
2
)
which is chosen when the withdrawal process is triggered.
While
OP_CHECKTEMPLATEVERIFY
is used in this proposal as
the preferred method to bind the proposed withdrawal to a particular set
of final outputs,
OP_VAULT
is composable with other (and
future) opcodes to facilitate other kinds of withdrawal processes.
The vault has a number of stages, some of them optional:
OP_VAULT
leaf
and one
OP_VAULT_RECOVER
leaf.
OP_VAULT
-tapleaf inputs into an output which is encumbered
by a timelocked withdrawal to a fixed set of outputs, chosen at trigger
time. This publicly broadcasts the intent to withdraw to some specific
set of outputs.
scriptPubKey
as the
OP_VAULT
-containing
input(s) being spent.
CHECKTEMPLATEVERIFY
hash),
after the trigger inputs have matured per the spend delay. Timelocked
CTV transactions are the motivating usage of OP_VAULT, but any script
template can be specified during the creation of the vault.
OP_VAULT_RECOVER
tapleaf to the prespecified
recovery path, which can be done at any point before the withdrawal
transaction confirms. Each input can optionally require a witness
satisfying a specified
recovery authorization
script, an
optional script prefixing the
OP_VAULT_RECOVER
fragment.
The use of recovery authorization has certain trade-offs discussed
later.
A primary consideration of this proposal is how fee management is handled. Providing dynamic fee management is critical to the operation of a vault, since
But dynamic fee management can introduce pinning vectors . Care has been taken to avoid unnecessarily introducing these vectors when using the new destination-based spending policies that this proposal introduces.
Originally, this proposal had a hard dependency on reformed transaction nVersion=3 policies, including ephemeral anchors, but it has since been revised to simply benefit from these changes in policy as well as other potential fee management mechanisms.
The tapscript opcodes
OP_SUCCESS187
(
0xbb
)
and
OP_SUCCESS188
(
0xbc
) are constrained with
new rules to implement
OP_VAULT
and
OP_VAULT_RECOVER
, respectively.
OP_VAULT
evaluation
When evaluating
OP_VAULT
(
OP_SUCCESS187
,
0xbb
), the expected format of the stack, shown top to
bottom, is:
<leaf-update-script-body>
<push-count>
[ <push-count> leaf-update script data items ... ]
<trigger-vout-idx>
<revault-vout-idx>
<revault-amount>
where
<leaf-update-script-body>
is a minimally-encoded
data push of a serialized script.
3
<push-count>
is an up to 4-byte minimally encoded
CScriptNum
indicating how many leaf-update script items
should be popped off the stack.
4
<push-count>
items on the stack, script execution
MUST fail and terminate immediately. In other words, after popping
<leaf-update-script-body>
, there must be at least
3 + <push-count>
items remaining on the stack.
<push-count>
stack items are popped
off the stack and prefixed as minimally-encoded push-data arguments to
the
<leaf-update-script-body>
to construct the
expected tapleaf replacement script.
<trigger-vout-idx>
is an up to 4-byte minimally
encoded
CScriptNum
indicating the index of the output
which, in conjunction with an optional revault output, carries forward
the value of this input, and has an identical taptree aside from the
currently executing leaf.
<revault-vout-idx>
is an up to 4-byte minimally
encoded
CScriptNum
optionally indicating the index of an
output which, in conjunction with the trigger output, carries forward
the value of this input, and has an identical scriptPubKey to the
current input.
<revault-amount>
is an up to 7-byte minimally
encoded CScriptNum indicating the number of satoshis being revaulted.
<revault-vout-idx>
is negative, script execution MUST fail and terminate immediately.
<revault-vout-idx>
is
not -1, script execution MUST fail and terminate immediately.
After the stack is parsed, the following validation checks are performed:
<trigger-vout-idx>
be called
triggerOut
.
<leaf-update-script-body>
and prefixing it with
minimally-encoded data pushes of the
<push-count>
leaf-update script data items be called the
leaf-update-script
.
<revault-vout-idx>
(if the index value is non-negative) be called
revaultOut
.
nValue
minus
<revault-amount>
are included within the output
nValue
found at
<trigger-vout-idx>
.
<revault-amount>
satoshis, if non-zero, are included
within the output's
nValue
found at
<revault-vout-idx>
.
TriggerCheck(input_amount, <revault-amount>, <trigger-vout-idx>, <revault-vout-idx>)
.
If none of the conditions fail, a single true value
(
0x01
) is left on the stack.
OP_VAULT_RECOVER
evaluation
When evaluating
OP_VAULT_RECOVER
(
OP_SUCCESS188
,
0xbc
), the expected format of
the stack, shown top to bottom, is:
<recovery-sPK-hash>
<recovery-vout-idx>
where
<recovery-sPK-hash>
is a 32-byte data push.
<recovery-vout-idx>
is an up to 4-byte minimally
encoded
CScriptNum
indicating the index of the recovery
output.
After the stack is parsed, the following validation checks are performed:
<recovery-vout-idx>
be
called
recoveryOut
.
output-sPK-hash = tagged_hash("VaultRecoverySPK", CompactSize(len(recoveryOut.scriptPubKey)) || recoveryOut.scriptPubKey)
,
where
tagged_hash()
is from the
BIP
340 reference code
.
output-sPK-hash
is not
equal to
<recovery-sPK-hash>
, script execution MUST
fail and terminate immediately.
nValue
greater than or equal to this input's amount, the
script SHOULD fail and terminate immediately.
nValue
of
recoveryOut
contains the entire
nValue
of this
input.
8
RecoveryCheck(<recovery-vout-idx>, input_amount)
.
If none of the conditions fail, a single true value
(
0x01
) is left on the stack.
Once all inputs for a transaction are validated per the rules above, any deferred checks queued MUST be evaluated.
The Python pseudocode for this is as follows:
class TriggerCheck:
"""Queued by evaluation of OP_VAULT (withdrawal trigger)."""
input_amount: int
revault_amount: int
trigger_vout_idx: int
revault_vout_idx: int
class RecoveryCheck:
"""Queued by evaluation of OP_VAULT_RECOVER."""
input_amount: int
vout_idx: int
def validate_deferred_checks(checks: [DeferredCheck], tx: Transaction) -> bool:
"""
Ensure that all value from vault inputs being triggered or recovered is preserved
in suitable output nValues.
"""
# Map to hold expected output values.
out_map: Dict[int, int] = defaultdict(lambda: 0)
for c in checks:
if isinstance(c, TriggerCheck):
out_map[c.trigger_vout_idx] += (c.input_amount - c.revault_amount)
if c.revault_amount > 0:
out_map[c.revault_vout_idx] += c.revault_amount
elif isinstance(c, RecoveryCheck):
out_map[c.vout_idx] += c.input_amount
for (vout_idx, amount_sats) in out_map.items():
# Trigger/recovery value can be greater than the constituent vault input
# amounts.
if tx.vout[vout_idx].nValue < amount_sats:
return False
return True
If the above procedure, or an equivalent, returns false, script execution MUST fail and terminate immediately.
This ensures that all compatible vault inputs can be batched into shared corresponding trigger or recovery outputs while preserving their entire input value.
In order to prevent possible pinning attacks, recovery transactions must be replaceable.
OP_VAULT_RECOVER
input being spent,
the script MUST fail (by policy, not consensus) and terminate
immediately if both
9
0xffffffff - 1
, per
BIP
125
, and
If the script containing
OP_VAULT_RECOVER
is 34 bytes or
less
10
, let it be called "unauthorized,"
because there is no script guarding the recovery process. In order to
prevent pinning attacks in the case of unauthorized recovery - since the
spend of the input (and the structure of the transaction) is not
authorized by a signed signature message - the output structure of
unauthorized recovery transaction is limited.
A sample implementation is available on bitcoin-inquisition here , with an associated pull request .
The specification above, perhaps surprisingly, does not specifically
cover how a relative timelocked withdrawal process with a fixed target
is implemented. The tapleaf update semantics specified in
OP_VAULT
as well as the output-based authorization enabled
by
OP_VAULT_RECOVER
can be used to implement a vault, but
they are incomplete without two other pieces:
OP_CHECKSEQUENCEVERIFY
, and
OP_CHECKTEMPLATEVERIFY
.
These two pieces are combined with the tapleaf update capabilities of
OP_VAULT
to create a vault, described below.
In order to vault coins, they can be spent into a witness v1
scriptPubKey
that contains a taptree of the form
tr(<internal-pubkey>,
leaves = {
recover:
<recovery-sPK-hash> OP_VAULT_RECOVER,
trigger:
<trigger-auth-pubkey> OP_CHECKSIGVERIFY (i)
<spend-delay> 2 $leaf-update-script-body OP_VAULT, (ii)
... [ possibly other leaves ]
}
)
where
$leaf-update-script-body
is, for example,
OP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKTEMPLATEVERIFY
.
OP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKSIG
to do a
time-delayed transfer of the coins to another key. This also
future-proofs
OP_VAULT
for future scripting
capabilities.
(i)
is called the "trigger
authorization," because it gates triggering the withdrawal. This can be
done in whatever manner the wallet designer would like.
(ii)
is the incomplete
OP_VAULT
invocation - it will be completed once the rest of
the parameters (the CTV target hash, trigger vout index, and revault
vout index) are provided by the trigger transaction witness.
Typically, the internal key for the vault taproot output will be specified so that it is controlled by the same descriptor as the recovery path, which facilitates another (though probably unused) means of recovering the vault output to the recovery path. This has the potential advantage of recovering the coin without ever revealing it was a vault.
Otherwise, the internal key can be chosen to be an unspendable NUMS point to force execution of the taptree contents.
To make use of the vault, and spend it towards some output, we
construct a spend of the above
tr()
output that simply
replaces the "trigger" leaf with the full leaf-update script (in this
case, a timelocked CTV script):
Witness stack:
- <revault-amount>
- <revault-vout-idx> (-1 if none)
- <trigger-vout-idx>
- <target-CTV-hash>
- <trigger-auth-pubkey-signature>
- [ "trigger" leaf script contents ]
- [ taproot control block prompting a script-path spend to "trigger" leaf ]
Output scripts:
[
tr(<internal-pubkey>,
leaves = {
recover:
<recovery-sPK-hash> OP_VAULT_RECOVER, <-- unchanged
trigger:
<target-CTV-hash> <spend-delay>
OP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKTEMPLATEVERIFY <-- changed per the
leaf-update
rules of OP_VAULT
... [ possibly other leaves ]
}
),
[ optional revault output with the
same sPK as the original vault output ],
]
OP_VAULT
has allowed the taptree to be transformed so
that the trigger leaf becomes a timelocked CTV script, which is what
actually facilitates the announced withdrawal. The withdrawal is
interruptible by the recovery path because the "recover" leaf is
preserved exactly from the original taptree.
Note that the CTV hash is specified at spend time using the witness
stack, and "locked in" via the
OP_VAULT
spend rules which
assert its existence in the output.
The vault funds can be recovered at any time prior to the spend of the timelocked CTV script by way of a script-path spend using the "recover" leaf.
When configuring a vault, the user must decide if they want to have
the recovery process gated by a script fragment prefixing the
OP_VAULT_RECOVER
instruction in the "recover" leaf. Its use
entails trade-offs.
Unauthorized recovery simplifies vault use in that recovery never
requires additional information aside from the location of the vault
outpoints and the recovery path - the "authorization" is simply the
reveal of the recovery path, i.e. the preimage of
<recovery-sPK-hash>
.
But because this reveal is the only authorization necessary to spend the vault coins to recovery, the user must expect to recover all such vaults at once, since an observer can replay this recovery (provided they know the outpoints).
Additionally, unauthorized recovery across multiple distinct recovery paths cannot be done in the same transaction, and fee control is more constrained: because the output structure is limited for unauthorized recovery, fee management relies either on inputs which are completely spent to fees or the use of the optional ephemeral anchor and package relay.
These limitations are to avoid pinning attacks.
With authorized recovery, the user must keep track of an additional piece of information: how to solve the recovery authorization script fragment when recovery is required.
If this key is lost, the user will be unable to initiate the recovery process for their coins. If an attacker obtains the recovery key, they may grief the user during the recovery process by constructing a low fee rate recovery transaction and broadcasting it (though they will not be able to pin because of the replaceability requirement on recovery transactions).
However, authorized recovery configurations have significant benefits. Batched recoveries are possible for vaults with otherwise incompatible recovery parameters. Fee management is much more flexible, since authorized recovery transactions are "free form" and unrelated inputs and outputs can be added, potentially to handle fees.
The benefits of batching and fee management that authorized recovery provides are significant. If the recovery authorization key falls into the hands of an attacker, the outcome is not catastrophic, whereas if the user loses their recovery authorization key as well as their trigger key, the result is likely coin loss. Consequently, the author's recommendation is to use a simple seed for the recovery authorization key that can be written down offline and replicated.
Note that the recovery authorization key is not the recovery path key, and this is much different than any recommendation on how to generate the recovery path key itself.
When creating a vault, four factors affect the resulting P2TR address:
The end user has the option of varying certain contents along descriptors in order to avoid reusing vault addresses without affecting key management, e.g. the trigger authorization pubkeys.
Note that when using unauthorized recovery, the reveal of the
recovery scriptPubKey will allow any observer to initiate the recovery
process for any vault with matching recovery params, provided they are
able to locate the vault outpoints. As a result, it is recommended to
expect that
all outputs sharing an identical unauthorized
<recovery-sPK-hash>
should be recovered
together
.
This situation can be avoided with a comparable key management model by varying the generation of each vault's recovery scriptPubKey along a single descriptor, but note that this will prevent recovering multiple separate vaults into a single recovery output.
Varying the internal pubkey will prevent batching the trigger of multiple vault inputs into a single trigger output; consequently it is recommended that users instead vary some component of the trigger leaf script if address reuse is undesirable. Users could vary the trigger pubkey along a descriptor, keeping the recovery path and internal-pubkey the same, which both avoids reusing addresses and allows batched trigger and recovery operations.
If using unauthorized recovery, it is recommended that you do not share recovery scriptPubKeys across separate trigger keys. If one trigger key is compromised, that will necessitate the (unauthorized) recovery of all vaults with that trigger key, which will reveal the recovery path preimage. This means that an observer might be able to initiate recovery for vaults controlled by an uncompromised trigger key.
Fees can be managed in a variety of ways, but it's worth noting that both trigger and recovery transactions must preserve the total value of vault inputs, so vaulted values cannot be repurposed to pay for fees. This does not apply to the withdrawal transaction, which can allocate value arbitrarily.
In the case of vaults that use recovery authorization, all transactions can "bring their own fees" in the form of unrelated inputs and outputs. These transactions are also free to specify ephemeral anchors, once the related relay policies are deployed. This means that vaults using recovery authorization have no dependence on the deploy of v3 relay policy.
For vaults using unauthorized recovery, the recovery transaction relies on the use of either fully-spent fee inputs or an ephemeral anchor output. This means that vaults which do not use recovery authorization are essentially dependent on v3 transaction relay policy being deployed.
OP_VAULT
outputs with the same taptree, aside from
slightly different trigger leaves, can be batched together in the same
withdrawal process. Two "trigger" leaves are compatible if they have the
same
OP_VAULT
arguments.
Note that this allows the trigger authorization -- the script
prefixing the
OP_VAULT
invocation -- to differ while still
allowing batching.
Trigger transactions can act on multiple incompatible
OP_VAULT
input sets, provided each set has a suitable
associated
triggerOut
output.
Since
SIGHASH_DEFAULT
can be used to sign the trigger
authorization, unrelated inputs and outputs can be included, possibly to
facilitate fee management or the batch withdrawal of incompatible
vaults.
During final withdrawal, multiple trigger outputs can be used towards
the same withdrawal transaction provided that they share identical
<target-CTV-hash>
parameters. This facilitates
batched withdrawals.
OP_VAULT_RECOVER
outputs with the same
<recovery-sPK-hash>
can be recovered into the same
output.
Recovery-incompatible vaults which have authorized recovery can be
recovered in the same transaction, so long as each set (grouped by
<recovery-sPK-hash>
) has an associated
recoveryOut
. This allows unrelated recoveries to share common
fee management.
The value of vaults is contingent upon having monitoring in place that will alert the owner when unexpected spends are taking place. This can be done in a variety of ways, with varying degrees of automation and trust in the watchtower.
In the maximum-trust case, the watchtower can be fully aware of all vaulted coins and has the means to initiate the recovery process if spends are not pre-reported to the watchtower.
In the minimum-trust case, the user can supply a probabilistic filter of which coins they wish to monitor; the watchtower would then alert the user if any coins matching the filter move, and the user would be responsible for ignoring false positives and handling recovery initiation.
Output descriptors for vault-related outputs will be covered in a subsequent BIP.
Activation mechanism is to be determined.
This BIP should be deployed concurrently with BIP 119 to enable full use of vaults.
OP_VAULT
and
OP_VAULT_RECOVER
replace,
respectively, the witness v1-only opcodes OP_SUCCESS187 and
OP_SUCCESS188 with stricter verification semantics. Consequently,
scripts using those opcodes which previously were valid will cease to be
valid with this change.
Stricter verification semantics for an OP_SUCCESSx opcode are a soft fork, so existing software will be fully functional without upgrade except for mining and block validation.
Backwards compatibility considerations are very comparable to previous deployments for OP_CHECKSEQUENCEVERIFY and OP_CHECKLOCKTIMEVERIFY (see BIP 65 and BIP 112 ).
The author would like to thank
OP_CHECKTEMPLATEVERIFY
(BIP
119) relied upon for this proposal?
During the withdrawal
process, the proposed final destination for value being withdrawn must
be committed to.
OP_CTV
is the simplest, safest way to
commit the spend of some coins to a particular set of outputs. An
earlier version of this proposal attempted to use a simpler, but similar
method, of locking the spend of coins to a set of outputs, but this
method introduced txid malleability.
OP_VAULT
<leaf-update-script-body>
with
no changes.
↩︎
leaf-update-script-body
with opcodes opens up
the door to prefix OP_SUCCESSX opcodes, to name a single issue only,
side-stepping the validation that was meant to be run by the committed
script.
↩︎
<recovery-sPK-hash> OP_VAULT_RECOVER
.
↩︎