7 from __future__
import annotations
10 MixOutputEvents, compute_nullifier, compute_commitment, receive_note
15 from zeth.core.utils import EtherValue, short_commitment, from_zeth_units
16 from zeth.api.zeth_messages_pb2
import ZethNote
17 from os.path
import join, basename, exists
18 from os
import makedirs
19 from shutil
import move
20 from typing
import Dict, List, Tuple, Optional, Iterator, Any, cast
28 SPENT_SUBDIRECTORY: str =
"spent"
29 MERKLE_TREE_FILE: str =
"merkle-tree.dat"
32 NullifierMap = Dict[str, str]
37 All secret data about a single ZethNote, including address in the merkle
38 tree and the commit value.
40 def __init__(self, note: ZethNote, address: int, commitment: bytes):
47 Returns the description in a form suitable for joinsplit.
57 return json.dumps(json_dict, indent=4)
60 def from_json(json_str: str) -> ZethNoteDescription:
61 json_dict = json.loads(json_str)
64 address=
int(json_dict[
"address"]),
65 commitment=bytes.fromhex(json_dict[
"commitment"]))
70 State to be saved in the wallet (excluding individual notes). As well as
71 the next block to query, we store some information about the state of the
72 Zeth deployment such as the number of notes or the number of distinct
73 addresses seen. This can be useful to estimate the security of a given
77 self, next_block: int, num_notes: int, nullifier_map: NullifierMap):
88 return json.dumps(json_dict, indent=4)
92 json_dict = json.loads(json_str)
94 next_block=
int(json_dict[
"next_block"]),
95 num_notes=
int(json_dict[
"num_notes"]),
96 nullifier_map=cast(NullifierMap, json_dict[
"nullifier_map"]))
99 def _load_state_or_default(state_file: str) -> WalletState:
100 if not exists(state_file):
102 with open(state_file,
"r")
as state_f:
103 return WalletState.from_json(state_f.read())
106 def _save_state(state_file: str, state: WalletState) ->
None:
107 with open(state_file,
"w")
as state_f:
108 state_f.write(state.to_json())
113 Very simple class to track the list of notes owned by a Zeth user.
115 Note: this class does not store the notes in encrypted form, and encodes
116 some information (including value) in the filename. It is a proof of
117 concept implementation and NOT intended to be secure against intruders who
118 have access to the file system. However, we expect that a secure
119 implementation could expose similar interface and functionality.
126 secret_address: ZethAddressPriv,
127 tree_hash: ITreeHash):
129 assert "_" not in username
133 self.
a_sk = secret_address.a_sk
135 self.
state_file = join(wallet_dir, f
"state_{username}")
139 join(wallet_dir, MERKLE_TREE_FILE),
140 int(math.pow(2, ZETH_MERKLE_TREE_DEPTH)),
148 out_ev: MixOutputEvents,
149 pp: PairingParameters) -> Optional[ZethNoteDescription]:
155 (commit, note) = our_note
156 if not _check_note(commit, note, pp):
164 self.
state.nullifier_map[nullifier.hex()] = \
170 output_events: List[MixOutputEvents],
171 pp: PairingParameters) -> List[ZethNoteDescription]:
173 Decrypt any notes we can, verify them as being valid, and store them in
179 for out_ev
in output_events:
181 f
"wallet.receive_notes: idx:{self.next_addr}, " +
182 f
"comm:{out_ev.commitment[:8].hex()}")
187 if note_desc
is not None:
188 new_notes.append(note_desc)
194 self.
state.num_notes = self.
state.num_notes + len(output_events)
200 Process nullifiers, marking any of our notes that they spend.
202 commits: List[str] = []
203 for nullifier
in nullifiers:
204 nullifier_hex = nullifier.hex()
205 short_commit = self.
state.nullifier_map.get(nullifier_hex,
None)
207 commits.append(short_commit)
214 Returns simple information that can be efficiently read from the notes
221 Returns simple info from note filenames in the spent directory.
227 return self.
state.next_block
230 self.
state.next_block = next_block
234 def find_note(self, note_id: str) -> ZethNoteDescription:
237 raise Exception(f
"no note with id {note_id}")
238 with open(note_file,
"r")
as note_f:
239 return ZethNoteDescription.from_json(note_f.read())
241 def _save_merkle_tree_if_changed(self) -> None:
247 def _write_note(self, note_desc: ZethNoteDescription) ->
None:
249 Write a note to the database (currently just a file-per-note).
252 with open(note_filename,
"w")
as note_f:
253 note_f.write(note_desc.to_json())
255 def _mark_note_spent(self, nullifier_hex: str, short_commit: str) ->
None:
257 Mark a note as having been spent. Find the file, move it to the `spent`
258 subdirectory, and remove the entry from the `nullifier_map`.
261 if note_file
is None:
262 raise Exception(f
"expected to find file for commit {short_commit}")
264 join(self.
wallet_dir, SPENT_SUBDIRECTORY, basename(note_file))
265 move(note_file, spent_file)
266 del self.
state.nullifier_map[nullifier_hex]
268 def _note_basename(self, note_desc: ZethNoteDescription) -> str:
271 return "note_%s_%04d_%s_%s" % (
272 self.
username, note_desc.address, cm_str, value_eth)
275 def _decode_basename(filename: str) -> Tuple[int, str, EtherValue]:
276 components = filename.split(
"_")
277 addr =
int(components[2])
278 short_commit = components[3]
280 return (addr, short_commit, value)
282 def _decode_note_files_in_dir(
283 self, dir_name: str) -> Iterator[Tuple[int, str, EtherValue]]:
284 wildcard = join(dir_name, f
"note_{self.username}_*")
285 filenames = sorted(glob.glob(wildcard))
286 for filename
in filenames:
294 def _find_note_file(self, key: str) -> Optional[str]:
296 Given some (fragment of) address or short commit, try to uniquely
297 identify a note file.
302 addr =
"%04d" %
int(key)
303 wildcard = f
"note_{self.username}_{addr}_*"
307 wildcard = f
"note_{self.username}_*_{key}_*"
309 candidates = list(glob.glob(join(self.
wallet_dir, wildcard)))
310 return candidates[0]
if len(candidates) == 1
else None
313 def _check_note(commit: bytes, note: ZethNote, pp: PairingParameters) -> bool:
315 Recalculate the note commitment and check that it matches `commit`, the
316 value emitted by the contract.
320 print(f
"WARN: bad commitment commit={commit.hex()}, cm={cm.hex()}")
325 def _ensure_dir(directory_name: str) ->
None:
326 if not exists(directory_name):
327 makedirs(directory_name)