Skip to content

Commit eb2509d

Browse files
committed
Added simplified vault construction with CCV only
1 parent 55c9239 commit eb2509d

File tree

3 files changed

+382
-1
lines changed

3 files changed

+382
-1
lines changed

examples/vault/README.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ The `vault.py` script provides a command-line and interactive interface for Bitc
44

55
Compared to that prototype, this version uses the `CCV_FLAG_DEDUCT_OUTPUT_AMOUNT` to allow partial revaulting of a Vault UTXO. This was the only missing feature as compared to the implementation using `OP_VAULT` (albeit with some differences in the revaulting semantics).
66

7+
It uses the `OP_CHECKCONTRACTVERIFY` and `OP_CHECKTEMPLATEVERIFY` opcodes.
8+
79
## Prerequisites
810

911
After following the [root prerequisites](../..#prerequisites), make sure to install the additional requirements:
@@ -25,7 +27,7 @@ $ python vault.py -m
2527
## Command-line Arguments
2628

2729
- `--mine-automatically` or `-m`: Enables automatic mining any time transactions are broadcast (assuming a wallet is loaded in bitcoin-core).
28-
- `--script` or `-s`: Executes commands from a specified script file, instead of running the interactive CLI interface. Some examples are in the (script)[scripts] folder.
30+
- `--script` or `-s`: Executes commands from a specified script file, instead of running the interactive CLI interface. Some examples are in the [scripts](scripts) folder.
2931

3032
## Interactive Commands
3133

@@ -47,3 +49,9 @@ The following commands implement specific features of the vault UTXOs (trigger,
4749
- `withdraw`: Completes the withdrawal from the vault; will fail if the timelock of 10 blocks is not satisfied.
4850

4951
The `scripts` folder has some example of interactions with the vault.
52+
53+
## Minivault
54+
55+
A simplified construction that only uses `OP_CHECKCONTRACTVERIFY` (without using `OP_CHECKTEMPLATEVERIFY`) is implemented in [minivault_contracts.py](minivault_contracts.py). It has all the same features of the full construction, except that the final withdrawal must necessarily go entirely to a single P2TR address.
56+
57+
Check out [test_minivault.py](../../tests/test_minivault.py) to see how it would be used.

examples/vault/minivault_contracts.py

+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
from dataclasses import dataclass
2+
from typing import Optional
3+
4+
from matt import CCV_FLAG_DEDUCT_OUTPUT_AMOUNT, NUMS_KEY
5+
from matt.argtypes import BytesType, IntType, SignerType
6+
from matt.btctools.script import OP_CHECKCONTRACTVERIFY, OP_CHECKSIG, OP_DUP, OP_PICK, OP_SWAP, OP_TRUE, CScript
7+
from matt.contracts import ClauseOutput, ClauseOutputAmountBehaviour, OpaqueP2TR, StandardClause, StandardP2TR, StandardAugmentedP2TR, ContractState
8+
from matt.script_helpers import check_input_contract, older
9+
10+
11+
class Vault(StandardP2TR):
12+
def __init__(self, alternate_pk: Optional[bytes], spend_delay: int, recover_pk: bytes, unvault_pk: bytes, *, has_partial_revault=True, has_early_recover=True):
13+
assert (alternate_pk is None or len(alternate_pk) == 32) and len(recover_pk) == 32 and len(unvault_pk)
14+
15+
self.alternate_pk = alternate_pk
16+
self.spend_delay = spend_delay
17+
self.recover_pk = recover_pk
18+
19+
unvaulting = Unvaulting(alternate_pk, spend_delay, recover_pk)
20+
21+
self.has_partial_revault = has_partial_revault
22+
self.has_early_recover = has_early_recover
23+
24+
# witness: <sig> <withdrawal_pk> <out_i>
25+
trigger = StandardClause(
26+
name="trigger",
27+
script=CScript([
28+
# data and index already on the stack
29+
0 if alternate_pk is None else alternate_pk, # pk
30+
unvaulting.get_taptree_merkle_root(), # taptree
31+
0, # standard flags
32+
OP_CHECKCONTRACTVERIFY,
33+
34+
unvault_pk,
35+
OP_CHECKSIG
36+
]),
37+
arg_specs=[
38+
('sig', SignerType(unvault_pk)),
39+
('withdrawal_pk', BytesType()),
40+
('out_i', IntType()),
41+
],
42+
next_outputs_fn=lambda args, _: [ClauseOutput(
43+
n=args['out_i'],
44+
next_contract=unvaulting,
45+
next_state=unvaulting.State(withdrawal_pk=args["withdrawal_pk"])
46+
)]
47+
)
48+
49+
# witness: <sig> <withdrawal_pk> <trigger_out_i> <revault_out_i>
50+
trigger_and_revault = StandardClause(
51+
name="trigger_and_revault",
52+
script=CScript([
53+
0, OP_SWAP, # no data tweak
54+
# <revault_out_i> from the witness
55+
-1, # current input's internal key
56+
-1, # current input's taptweak
57+
CCV_FLAG_DEDUCT_OUTPUT_AMOUNT, # revault output
58+
OP_CHECKCONTRACTVERIFY,
59+
60+
# data and index already on the stack
61+
0 if alternate_pk is None else alternate_pk, # pk
62+
unvaulting.get_taptree_merkle_root(), # taptree
63+
0, # standard flags
64+
OP_CHECKCONTRACTVERIFY,
65+
66+
unvault_pk,
67+
OP_CHECKSIG
68+
]),
69+
arg_specs=[
70+
('sig', SignerType(unvault_pk)),
71+
('withdrawal_pk', BytesType()),
72+
('out_i', IntType()),
73+
('revault_out_i', IntType()),
74+
],
75+
next_outputs_fn=lambda args, _: [
76+
ClauseOutput(n=args['revault_out_i'], next_contract=self,
77+
next_amount=ClauseOutputAmountBehaviour.DEDUCT_OUTPUT),
78+
ClauseOutput(
79+
n=args['out_i'],
80+
next_contract=unvaulting,
81+
next_state=unvaulting.State(withdrawal_pk=args["withdrawal_pk"])),
82+
]
83+
)
84+
85+
# witness: <out_i>
86+
recover = StandardClause(
87+
name="recover",
88+
script=CScript([
89+
0, # data
90+
OP_SWAP, # <out_i> (from witness)
91+
recover_pk, # pk
92+
0, # taptree
93+
0, # flags
94+
OP_CHECKCONTRACTVERIFY,
95+
OP_TRUE
96+
]),
97+
arg_specs=[
98+
('out_i', IntType()),
99+
],
100+
next_outputs_fn=lambda args, _: [ClauseOutput(n=args['out_i'], next_contract=OpaqueP2TR(recover_pk))]
101+
)
102+
103+
if self.has_partial_revault:
104+
if self.has_early_recover:
105+
clauses = [trigger, [trigger_and_revault, recover]]
106+
else:
107+
clauses = [trigger, trigger_and_revault]
108+
else:
109+
if self.has_early_recover:
110+
clauses = [trigger, recover]
111+
else:
112+
clauses = trigger
113+
114+
super().__init__(NUMS_KEY if alternate_pk is None else alternate_pk, clauses)
115+
116+
117+
class Unvaulting(StandardAugmentedP2TR):
118+
@dataclass
119+
class State(ContractState):
120+
withdrawal_pk: bytes
121+
122+
def encode(self):
123+
return self.withdrawal_pk
124+
125+
def encoder_script():
126+
return CScript([])
127+
128+
def __init__(self, alternate_pk: Optional[bytes], spend_delay: int, recover_pk: bytes):
129+
assert (alternate_pk is None or len(alternate_pk) == 32) and len(recover_pk) == 32
130+
131+
self.alternate_pk = alternate_pk
132+
self.spend_delay = spend_delay
133+
self.recover_pk = recover_pk
134+
135+
# witness: <withdrawal_pk>
136+
withdrawal = StandardClause(
137+
name="withdraw",
138+
script=CScript([
139+
OP_DUP,
140+
141+
*check_input_contract(-1, alternate_pk),
142+
143+
# Check timelock
144+
*older(self.spend_delay),
145+
146+
# Check that the transaction output is as expected
147+
0, # no data
148+
0, # output index
149+
2, OP_PICK, # withdrawal_pk
150+
0, # no taptweak
151+
0, # default flags
152+
OP_CHECKCONTRACTVERIFY,
153+
154+
# withdrawal_pk is left on the stack on success
155+
]),
156+
arg_specs=[
157+
('withdrawal_pk', BytesType())
158+
],
159+
next_outputs_fn=lambda args, _: [ClauseOutput(n=0, next_contract=OpaqueP2TR(args['withdrawal_pk']))]
160+
)
161+
162+
# witness: <out_i>
163+
recover = StandardClause(
164+
name="recover",
165+
script=CScript([
166+
0, # data
167+
OP_SWAP, # <out_i> (from witness)
168+
recover_pk, # pk
169+
0, # taptree
170+
0, # flags
171+
OP_CHECKCONTRACTVERIFY,
172+
OP_TRUE
173+
]),
174+
arg_specs=[
175+
('out_i', IntType()),
176+
],
177+
next_outputs_fn=lambda args, _: [ClauseOutput(n=args['out_i'], next_contract=OpaqueP2TR(recover_pk))]
178+
)
179+
180+
super().__init__(NUMS_KEY if alternate_pk is None else alternate_pk, [withdrawal, recover])

0 commit comments

Comments
 (0)