Skip to content

Commit 55ed8f3

Browse files
committed
Add RAM contract
1 parent 697ec85 commit 55ed8f3

File tree

9 files changed

+789
-32
lines changed

9 files changed

+789
-32
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ $ pip install .
2323
The fastest way to get started is [this docker container](https://github.com/Merkleize/docker):
2424

2525
```bash
26-
$ docker pull docker pull bigspider/bitcoin_matt
26+
$ docker pull bigspider/bitcoin_matt
2727

2828
$ docker run -d -p 18443:18443 bigspider/bitcoin_matt
2929
```

examples/ram/README.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# RAM
2+
3+
`ram.py` is a simple contract that allows a Script to commit to some memory, and modify it in successive executions.
4+
5+
It is a building block for more complex smart contracts that require "memory" access.
6+
7+
## Prerequisites
8+
9+
After following the [root prerequisites](../..#prerequisites), make sure to install the additional requirements:
10+
11+
```bash
12+
$ pip install -r requirements.txt
13+
```
14+
15+
## How to Run
16+
17+
`ram.py` is a command line tool that allows to create, manage and spend the Vault UTXOs.
18+
19+
To run the script, navigate to the directory containing `vault.py` and use the following command:
20+
21+
```bash
22+
$ python ram.py -m
23+
```
24+
25+
## Command-line Arguments
26+
27+
- `--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.
29+
30+
## Interactive Commands
31+
32+
While typing commands in interactive mode, the script offers auto-completion features to assist you.
33+
34+
You can use the following commands to work with regtest:
35+
- `fund`: Funds the vault with a specified amount.
36+
- `mine [n]`: mines 1 or `n` blocks.
37+
38+
The following commands allows to inspect the current state and history of known UTXOs:
39+
40+
- `list`: Lists available UTXOs known to the ContractManager.
41+
- `printall`: Prints in a nice formats for Markdown all the transactions from known UTXOs.
42+
43+
The following commands implement specific features of the vault UTXOs (trigger, recover, withdraw). Autocompletion can help
44+
45+
- `withdraw`: Given the proof for the value of an element, withdraw from the contract.
46+
- `write i value`: Given a valid proof for the value of the `i`-th element, updates the state but replacing it with `value`.

examples/ram/ram.py

+279
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import argparse
2+
import json
3+
4+
import os
5+
6+
import logging
7+
import shlex
8+
import traceback
9+
10+
from dotenv import load_dotenv
11+
12+
from prompt_toolkit import prompt
13+
from prompt_toolkit.completion import Completer, Completion
14+
from prompt_toolkit.history import FileHistory
15+
16+
from matt.btctools.auth_proxy import AuthServiceProxy
17+
18+
from matt.btctools import key
19+
from matt.btctools.messages import CTransaction, CTxIn, CTxOut, sha256
20+
from matt.btctools.segwit_addr import decode_segwit_address
21+
from matt.environment import Environment
22+
from matt import ContractInstance, ContractInstanceStatus, ContractManager
23+
from matt.utils import print_tx
24+
from matt.merkle import MerkleTree
25+
26+
from ram_contracts import RAM
27+
28+
logging.basicConfig(filename='matt-cli.log', level=logging.DEBUG)
29+
30+
31+
class ActionArgumentCompleter(Completer):
32+
ACTION_ARGUMENTS = {
33+
"fund": ["amount="],
34+
"list": [],
35+
"printall": [],
36+
"withdraw": ["item=", "leaf_index=", "outputs=\"["],
37+
"write": ["item=", "leaf_index=", "new_value="],
38+
}
39+
40+
def get_completions(self, document, complete_event):
41+
word_before_cursor = document.get_word_before_cursor(WORD=True)
42+
43+
if ' ' not in document.text:
44+
# user is typing the action
45+
for action in self.ACTION_ARGUMENTS.keys():
46+
if action.startswith(word_before_cursor):
47+
yield Completion(action, start_position=-len(word_before_cursor))
48+
else:
49+
# user is typing an argument, find which are valid
50+
action = document.text.split()[0]
51+
for argument in self.ACTION_ARGUMENTS.get(action, []):
52+
if argument not in document.text and argument.startswith(word_before_cursor):
53+
yield Completion(argument, start_position=-len(word_before_cursor))
54+
55+
56+
# point with provably unknown private key
57+
NUMS_KEY: bytes = bytes.fromhex("50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0")
58+
59+
load_dotenv()
60+
61+
rpc_user = os.getenv("RPC_USER", "rpcuser")
62+
rpc_password = os.getenv("RPC_PASSWORD", "rpcpass")
63+
rpc_host = os.getenv("RPC_HOST", "localhost")
64+
rpc_port = os.getenv("RPC_PORT", 18443)
65+
66+
67+
def segwit_addr_to_scriptpubkey(addr: str) -> bytes:
68+
wit_ver, wit_prog = decode_segwit_address("bcrt", addr)
69+
70+
if wit_ver is None or wit_prog is None:
71+
raise ValueError(f"Invalid segwit address (or wrong network): {addr}")
72+
73+
return bytes([
74+
wit_ver + (0x50 if wit_ver > 0 else 0),
75+
len(wit_prog),
76+
*wit_prog
77+
])
78+
79+
80+
def parse_outputs(output_strings: list[str]) -> list[tuple[str, int]]:
81+
"""Parses a list of strings in the form "address:amount" into a list of (address, amount) tuples.
82+
83+
Args:
84+
- output_strings (list of str): List of strings in the form "address:amount".
85+
86+
Returns:
87+
- list of (str, int): List of (address, amount) tuples.
88+
"""
89+
outputs = []
90+
for output_str in output_strings:
91+
address, amount_str = output_str.split(":")
92+
amount = int(amount_str)
93+
if amount <= 0:
94+
raise ValueError(f"Invalid amount for address {address}: {amount_str}")
95+
outputs.append((address, amount))
96+
return outputs
97+
98+
99+
def execute_command(input_line: str):
100+
# consider lines starting with '#' (possibly prefixed with whitespaces) as comments
101+
if input_line.strip().startswith("#"):
102+
return
103+
104+
# Split into a command and the list of arguments
105+
try:
106+
input_line_list = shlex.split(input_line)
107+
except ValueError as e:
108+
print(f"Invalid command: {str(e)}")
109+
return
110+
111+
# Ensure input_line_list is not empty
112+
if input_line_list:
113+
action = input_line_list[0].strip()
114+
else:
115+
return
116+
117+
# Get the necessary arguments from input_command_list
118+
args_dict = {}
119+
pos_count = 0 # count of positional arguments
120+
for item in input_line_list[1:]:
121+
parts = item.strip().split('=', 1)
122+
if len(parts) == 2:
123+
param, value = parts
124+
args_dict[param] = value
125+
else:
126+
# record positional arguments with keys @0, @1, ...
127+
args_dict['@' + str(pos_count)] = parts[0]
128+
pos_count += 1
129+
130+
if action == "":
131+
return
132+
elif action not in actions:
133+
print("Invalid action")
134+
return
135+
elif action == "list":
136+
for i, instance in enumerate(manager.instances):
137+
print(i, instance.status, instance)
138+
elif action == "mine":
139+
if '@0' in args_dict:
140+
n_blocks = int(args_dict['@0'])
141+
else:
142+
n_blocks = 1
143+
print(repr(manager._mine_blocks(n_blocks)))
144+
elif action == "printall":
145+
all_txs = {}
146+
for i, instance in enumerate(manager.instances):
147+
if instance.spending_tx is not None:
148+
all_txs[instance.spending_tx.hash] = (instance.contract.__class__.__name__, instance.spending_tx)
149+
150+
for msg, tx in all_txs.values():
151+
print_tx(tx, msg)
152+
elif action == "withdraw":
153+
item_index = int(args_dict["item"])
154+
leaf_index = int(args_dict["leaf_index"])
155+
outputs = parse_outputs(json.loads(args_dict["outputs"]))
156+
157+
if item_index not in range(len(manager.instances)):
158+
raise ValueError("Invalid item")
159+
160+
R_inst = manager.instances[item_index]
161+
mt = MerkleTree(R_inst.data_expanded)
162+
163+
if leaf_index not in range(len(R_inst.data_expanded)):
164+
raise ValueError("Invalid leaf index")
165+
166+
args = {
167+
"merkle_root": mt.root,
168+
"merkle_proof": mt.prove_leaf(leaf_index)
169+
}
170+
171+
spend_tx, _ = manager.get_spend_tx(
172+
(
173+
manager.instances[item_index],
174+
"withdraw",
175+
args
176+
)
177+
)
178+
179+
# TODO: make utility function to create the vout easily
180+
spend_tx.vout = []
181+
for address, amount in outputs:
182+
spend_tx.vout.append(CTxOut(
183+
nValue=amount,
184+
scriptPubKey=segwit_addr_to_scriptpubkey(address)
185+
)
186+
)
187+
188+
spend_tx.wit.vtxinwit = [manager.get_spend_wit(
189+
R_inst,
190+
"withdraw",
191+
args
192+
)]
193+
194+
print(mt.prove_leaf(leaf_index))
195+
print(spend_tx) # TODO: remove
196+
197+
result = manager.spend_and_wait(R_inst, spend_tx)
198+
199+
print("Done")
200+
elif action == "write":
201+
# TODO
202+
raise NotImplementedError
203+
elif action == "fund":
204+
amount = int(args_dict["amount"])
205+
R_inst = ContractInstance(R)
206+
R_inst.data_expanded = list(map(lambda x : sha256(x.to_bytes(1, byteorder='little')), range(R.size)))
207+
R_inst.data = MerkleTree(R_inst.data_expanded).root
208+
209+
manager.add_instance(R_inst)
210+
txid = rpc.sendtoaddress(R_inst.get_address(), amount/100_000_000)
211+
print(f"Waiting for funding transaction {txid} to be confirmed...")
212+
manager.wait_for_outpoint(R_inst, txid)
213+
print(R_inst.funding_tx)
214+
215+
216+
def cli_main():
217+
completer = ActionArgumentCompleter()
218+
# Create a history object
219+
history = FileHistory('.cli-history')
220+
221+
while True:
222+
try:
223+
input_line = prompt("₿ ", history=history, completer=completer)
224+
execute_command(input_line)
225+
except (KeyboardInterrupt, EOFError):
226+
raise # exit
227+
except Exception as err:
228+
print(f"Error: {err}")
229+
print(traceback.format_exc())
230+
231+
232+
def script_main(script_filename: str):
233+
with open(script_filename, "r") as script_file:
234+
for input_line in script_file:
235+
try:
236+
# Assuming each command can be executed in a similar manner to the CLI
237+
# This will depend on the structure of the main() function and may need adjustments
238+
execute_command(input_line)
239+
except Exception as e:
240+
print(f"Error executing command: {input_line.strip()} - Error: {str(e)}")
241+
break
242+
243+
244+
if __name__ == "__main__":
245+
parser = argparse.ArgumentParser()
246+
247+
# Mine automatically option
248+
parser.add_argument("--mine-automatically", "-m", action="store_true", help="Mine automatically")
249+
250+
# Script file option
251+
parser.add_argument("--script", "-s", type=str, help="Execute commands from script file")
252+
253+
args = parser.parse_args()
254+
255+
actions = ["fund", "mine", "list", "printall", "withdraw"]
256+
257+
unvault_priv_key = key.ExtendedKey.deserialize(
258+
"tprv8ZgxMBicQKsPdpwA4vW8DcSdXzPn7GkS2RdziGXUX8k86bgDQLKhyXtB3HMbJhPFd2vKRpChWxgPe787WWVqEtjy8hGbZHqZKeRrEwMm3SN")
259+
recover_priv_key = key.ExtendedKey.deserialize(
260+
"tprv8ZgxMBicQKsPeDvaW4xxmiMXxqakLgvukT8A5GR6mRwBwjsDJV1jcZab8mxSerNcj22YPrusm2Pz5oR8LTw9GqpWT51VexTNBzxxm49jCZZ")
261+
262+
rpc = AuthServiceProxy(f"http://{rpc_user}:{rpc_password}@{rpc_host}:{rpc_port}")
263+
264+
R = RAM(8)
265+
266+
manager = ContractManager([], rpc, mine_automatically=args.mine_automatically)
267+
environment = Environment(rpc, manager, None, None, False)
268+
269+
# map from known ctv hashes to the corresponding template (used for withdrawals)
270+
ctv_templates: dict[bytes, CTransaction] = {}
271+
272+
273+
if args.script:
274+
script_main(args.script)
275+
else:
276+
try:
277+
cli_main()
278+
except (KeyboardInterrupt, EOFError):
279+
pass # exit

0 commit comments

Comments
 (0)