Anthony Towns [ARCHIVE] on Nostr: 📅 Original date posted:2023-01-10 🗒️ Summary of this message: A proposed ...
📅 Original date posted:2023-01-10
🗒️ Summary of this message: A proposed scheme for securing Bitcoin funds involves a scriptPubKey with two spend paths: recovery and unvaulting, and a cold wallet for freezing funds.
📝 Original message:On Mon, Jan 09, 2023 at 11:07:54AM -0500, James O'Beirne via bitcoin-dev wrote:
> 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
🗒️ Summary of this message: A proposed scheme for securing Bitcoin funds involves a scriptPubKey with two spend paths: recovery and unvaulting, and a cold wallet for freezing funds.
📝 Original message:On Mon, Jan 09, 2023 at 11:07:54AM -0500, James O'Beirne via bitcoin-dev wrote:
> 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