Zeth - Zerocash on Ethereum  0.8
Reference implementation of the Zeth protocol by Clearmatics
wallet.py
Go to the documentation of this file.
1 #!/usr/bin/env python3
2 
3 # Copyright (c) 2015-2022 Clearmatics Technologies Ltd
4 #
5 # SPDX-License-Identifier: LGPL-3.0+
6 
7 from __future__ import annotations
8 from zeth.core.zeth_address import ZethAddressPriv
9 from zeth.core.mixer_client import \
10  MixOutputEvents, compute_nullifier, compute_commitment, receive_note
11 from zeth.core.proto_utils import zeth_note_from_json_dict, zeth_note_to_json_dict
12 from zeth.core.constants import ZETH_MERKLE_TREE_DEPTH
13 from zeth.core.pairing import PairingParameters
14 from zeth.core.merkle_tree import ITreeHash, PersistentMerkleTree
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
21 import glob
22 import json
23 import math
24 
25 
26 # pylint: disable=too-many-instance-attributes
27 
28 SPENT_SUBDIRECTORY: str = "spent"
29 MERKLE_TREE_FILE: str = "merkle-tree.dat"
30 
31 # Map nullifier to short commitment string identifying the commitment.
32 NullifierMap = Dict[str, str]
33 
34 
36  """
37  All secret data about a single ZethNote, including address in the merkle
38  tree and the commit value.
39  """
40  def __init__(self, note: ZethNote, address: int, commitment: bytes):
41  self.note = note
42  self.address = address
43  self.commitment = commitment
44 
45  def as_input(self) -> Tuple[int, ZethNote]:
46  """
47  Returns the description in a form suitable for joinsplit.
48  """
49  return (self.address, self.note)
50 
51  def to_json(self) -> str:
52  json_dict = {
53  "note": zeth_note_to_json_dict(self.note),
54  "address": str(self.address),
55  "commitment": self.commitment.hex(),
56  }
57  return json.dumps(json_dict, indent=4)
58 
59  @staticmethod
60  def from_json(json_str: str) -> ZethNoteDescription:
61  json_dict = json.loads(json_str)
62  return ZethNoteDescription(
63  note=zeth_note_from_json_dict(json_dict["note"]),
64  address=int(json_dict["address"]),
65  commitment=bytes.fromhex(json_dict["commitment"]))
66 
67 
69  """
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
74  transaction.
75  """
76  def __init__(
77  self, next_block: int, num_notes: int, nullifier_map: NullifierMap):
78  self.next_block = next_block
79  self.num_notes = num_notes
80  self.nullifier_map = nullifier_map
81 
82  def to_json(self) -> str:
83  json_dict = {
84  "next_block": self.next_block,
85  "num_notes": self.num_notes,
86  "nullifier_map": self.nullifier_map,
87  }
88  return json.dumps(json_dict, indent=4)
89 
90  @staticmethod
91  def from_json(json_str: str) -> WalletState:
92  json_dict = json.loads(json_str)
93  return WalletState(
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"]))
97 
98 
99 def _load_state_or_default(state_file: str) -> WalletState:
100  if not exists(state_file):
101  return WalletState(1, 0, {})
102  with open(state_file, "r") as state_f:
103  return WalletState.from_json(state_f.read())
104 
105 
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())
109 
110 
111 class Wallet:
112  """
113  Very simple class to track the list of notes owned by a Zeth user.
114 
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.
120  """
121  def __init__(
122  self,
123  mixer_instance: Any,
124  username: str,
125  wallet_dir: str,
126  secret_address: ZethAddressPriv,
127  tree_hash: ITreeHash):
128  # k_sk_receiver: EncryptionSecretKey):
129  assert "_" not in username
130  self.mixer_instance = mixer_instance
131  self.username = username
132  self.wallet_dir = wallet_dir
133  self.a_sk = secret_address.a_sk
134  self.k_sk_receiver = secret_address.k_sk
135  self.state_file = join(wallet_dir, f"state_{username}")
136  self.state = _load_state_or_default(self.state_file)
137  _ensure_dir(join(self.wallet_dir, SPENT_SUBDIRECTORY))
138  self.merkle_tree = PersistentMerkleTree.open(
139  join(wallet_dir, MERKLE_TREE_FILE),
140  int(math.pow(2, ZETH_MERKLE_TREE_DEPTH)),
141  tree_hash)
142  self.merkle_tree_changed = False
143  self.next_addr = self.merkle_tree.get_num_entries()
144 
146  self,
147  comm_addr: int,
148  out_ev: MixOutputEvents,
149  pp: PairingParameters) -> Optional[ZethNoteDescription]:
150  # Check this output event to see if it belongs to this wallet.
151  our_note = receive_note(out_ev, self.k_sk_receiver)
152  if our_note is None:
153  return None
154 
155  (commit, note) = our_note
156  if not _check_note(commit, note, pp):
157  return None
158 
159  note_desc = ZethNoteDescription(note, comm_addr, commit)
160  self._write_note(note_desc)
161 
162  # Add the nullifier to the map in the state file
163  nullifier = compute_nullifier(note_desc.note, self.a_sk)
164  self.state.nullifier_map[nullifier.hex()] = \
165  short_commitment(commit)
166  return note_desc
167 
169  self,
170  output_events: List[MixOutputEvents],
171  pp: PairingParameters) -> List[ZethNoteDescription]:
172  """
173  Decrypt any notes we can, verify them as being valid, and store them in
174  the database.
175  """
176  new_notes = []
177 
178  self.merkle_tree_changed = len(output_events) != 0
179  for out_ev in output_events:
180  print(
181  f"wallet.receive_notes: idx:{self.next_addr}, " +
182  f"comm:{out_ev.commitment[:8].hex()}")
183 
184  # All commitments must be added to the tree in order.
185  self.merkle_tree.insert(out_ev.commitment)
186  note_desc = self.receive_note(self.next_addr, out_ev, pp)
187  if note_desc is not None:
188  new_notes.append(note_desc)
189 
190  self.next_addr = self.next_addr + 1
191 
192  # Record full set of notes seen to keep an estimate of the total in the
193  # mixer.
194  self.state.num_notes = self.state.num_notes + len(output_events)
195 
196  return new_notes
197 
198  def mark_nullifiers_used(self, nullifiers: List[bytes]) -> List[str]:
199  """
200  Process nullifiers, marking any of our notes that they spend.
201  """
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)
206  if short_commit:
207  commits.append(short_commit)
208  self._mark_note_spent(nullifier_hex, short_commit)
209 
210  return commits
211 
212  def note_summaries(self) -> Iterator[Tuple[int, str, EtherValue]]:
213  """
214  Returns simple information that can be efficiently read from the notes
215  store.
216  """
217  return self._decode_note_files_in_dir(self.wallet_dir)
218 
219  def spent_note_summaries(self) -> Iterator[Tuple[int, str, EtherValue]]:
220  """
221  Returns simple info from note filenames in the spent directory.
222  """
223  return self._decode_note_files_in_dir(
224  join(self.wallet_dir, SPENT_SUBDIRECTORY))
225 
226  def get_next_block(self) -> int:
227  return self.state.next_block
228 
229  def update_and_save_state(self, next_block: int) -> None:
230  self.state.next_block = next_block
231  _save_state(self.state_file, self.state)
233 
234  def find_note(self, note_id: str) -> ZethNoteDescription:
235  note_file = self._find_note_file(note_id)
236  if not note_file:
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())
240 
241  def _save_merkle_tree_if_changed(self) -> None:
242  if self.merkle_tree_changed:
243  self.merkle_tree_changed = False
244  self.merkle_tree.recompute_root()
245  self.merkle_tree.save()
246 
247  def _write_note(self, note_desc: ZethNoteDescription) -> None:
248  """
249  Write a note to the database (currently just a file-per-note).
250  """
251  note_filename = join(self.wallet_dir, self._note_basename(note_desc))
252  with open(note_filename, "w") as note_f:
253  note_f.write(note_desc.to_json())
254 
255  def _mark_note_spent(self, nullifier_hex: str, short_commit: str) -> None:
256  """
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`.
259  """
260  note_file = self._find_note_file(short_commit)
261  if note_file is None:
262  raise Exception(f"expected to find file for commit {short_commit}")
263  spent_file = \
264  join(self.wallet_dir, SPENT_SUBDIRECTORY, basename(note_file))
265  move(note_file, spent_file)
266  del self.state.nullifier_map[nullifier_hex]
267 
268  def _note_basename(self, note_desc: ZethNoteDescription) -> str:
269  value_eth = from_zeth_units(int(note_desc.note.value, 16)).ether()
270  cm_str = short_commitment(note_desc.commitment)
271  return "note_%s_%04d_%s_%s" % (
272  self.username, note_desc.address, cm_str, value_eth)
273 
274  @staticmethod
275  def _decode_basename(filename: str) -> Tuple[int, str, EtherValue]:
276  components = filename.split("_")
277  addr = int(components[2])
278  short_commit = components[3]
279  value = EtherValue(components[4], 'ether')
280  return (addr, short_commit, value)
281 
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:
287  try:
288  yield self._decode_basename(basename(filename))
289  # print(f"wallet: _decoded_note_filenames: file={filename}")
290  except ValueError:
291  # print(f"wallet: _decoded_note_filenames: FAILED {filename}")
292  continue
293 
294  def _find_note_file(self, key: str) -> Optional[str]:
295  """
296  Given some (fragment of) address or short commit, try to uniquely
297  identify a note file.
298  """
299  # If len <= 4, assume it's an address, otherwise a commit
300  if len(key) < 5:
301  try:
302  addr = "%04d" % int(key)
303  wildcard = f"note_{self.username}_{addr}_*"
304  except Exception:
305  return None
306  else:
307  wildcard = f"note_{self.username}_*_{key}_*"
308 
309  candidates = list(glob.glob(join(self.wallet_dir, wildcard)))
310  return candidates[0] if len(candidates) == 1 else None
311 
312 
313 def _check_note(commit: bytes, note: ZethNote, pp: PairingParameters) -> bool:
314  """
315  Recalculate the note commitment and check that it matches `commit`, the
316  value emitted by the contract.
317  """
318  cm = compute_commitment(note, pp)
319  if commit != cm:
320  print(f"WARN: bad commitment commit={commit.hex()}, cm={cm.hex()}")
321  return False
322  return True
323 
324 
325 def _ensure_dir(directory_name: str) -> None:
326  if not exists(directory_name):
327  makedirs(directory_name)
zeth.core.wallet.Wallet.mixer_instance
mixer_instance
Definition: wallet.py:124
zeth.core.wallet.WalletState.from_json
WalletState from_json(str json_str)
Definition: wallet.py:91
zeth.core.wallet.ZethNoteDescription.commitment
commitment
Definition: wallet.py:43
zeth.core.wallet.Wallet.a_sk
a_sk
Definition: wallet.py:127
zeth.core.wallet.Wallet.k_sk_receiver
k_sk_receiver
Definition: wallet.py:128
zeth.cli.zeth_deploy.int
int
Definition: zeth_deploy.py:27
zeth.core.wallet.WalletState.nullifier_map
nullifier_map
Definition: wallet.py:79
zeth.core.wallet.Wallet.note_summaries
Iterator[Tuple[int, str, EtherValue]] note_summaries(self)
Definition: wallet.py:212
zeth.core.wallet.Wallet
Definition: wallet.py:111
zeth.core.merkle_tree
Definition: merkle_tree.py:1
zeth.core.wallet.Wallet._write_note
None _write_note(self, ZethNoteDescription note_desc)
Definition: wallet.py:247
zeth.core.wallet.ZethNoteDescription
Definition: wallet.py:35
zeth.core.wallet.WalletState
Definition: wallet.py:68
zeth.core.wallet.Wallet._note_basename
str _note_basename(self, ZethNoteDescription note_desc)
Definition: wallet.py:268
zeth.core.wallet.ZethNoteDescription.address
address
Definition: wallet.py:42
zeth.core.wallet.Wallet.update_and_save_state
None update_and_save_state(self, int next_block)
Definition: wallet.py:229
zeth.core.wallet.Wallet.merkle_tree_changed
merkle_tree_changed
Definition: wallet.py:136
zeth.core.constants
Definition: constants.py:1
zeth.core.wallet.Wallet.merkle_tree
merkle_tree
Definition: wallet.py:132
zeth.core.wallet.Wallet.__init__
def __init__(self, Any mixer_instance, str username, str wallet_dir, ZethAddressPriv secret_address, ITreeHash tree_hash)
Definition: wallet.py:121
zeth.core.wallet.Wallet._save_merkle_tree_if_changed
None _save_merkle_tree_if_changed(self)
Definition: wallet.py:241
zeth.core.wallet.ZethNoteDescription.to_json
str to_json(self)
Definition: wallet.py:51
zeth.core.mixer_client
Definition: mixer_client.py:1
zeth.core.wallet.Wallet.mark_nullifiers_used
List[str] mark_nullifiers_used(self, List[bytes] nullifiers)
Definition: wallet.py:198
zeth.core.wallet.WalletState.to_json
str to_json(self)
Definition: wallet.py:82
zeth.core.wallet.Wallet.state
state
Definition: wallet.py:130
zeth.core.wallet.ZethNoteDescription.note
note
Definition: wallet.py:41
zeth.core.wallet.Wallet.spent_note_summaries
Iterator[Tuple[int, str, EtherValue]] spent_note_summaries(self)
Definition: wallet.py:219
zeth.core.wallet.Wallet.find_note
ZethNoteDescription find_note(self, str note_id)
Definition: wallet.py:234
zeth.core.wallet.Wallet.receive_note
Optional[ZethNoteDescription] receive_note(self, int comm_addr, MixOutputEvents out_ev, PairingParameters pp)
Definition: wallet.py:145
zeth.core.wallet.Wallet._decode_basename
Tuple[int, str, EtherValue] _decode_basename(str filename)
Definition: wallet.py:275
zeth.core.wallet.str
str
Definition: wallet.py:28
zeth.core.utils
Definition: utils.py:1
zeth.core.wallet.WalletState.__init__
def __init__(self, int next_block, int num_notes, NullifierMap nullifier_map)
Definition: wallet.py:76
zeth.core.wallet.Wallet.get_next_block
int get_next_block(self)
Definition: wallet.py:226
zeth.core.wallet.ZethNoteDescription.as_input
Tuple[int, ZethNote] as_input(self)
Definition: wallet.py:45
zeth.core.pairing
Definition: pairing.py:1
zeth.core.wallet.WalletState.num_notes
num_notes
Definition: wallet.py:78
zeth.core.wallet.ZethNoteDescription.from_json
ZethNoteDescription from_json(str json_str)
Definition: wallet.py:60
zeth.core.utils.short_commitment
str short_commitment(bytes cm)
Definition: utils.py:306
zeth.core.wallet.Wallet.receive_notes
List[ZethNoteDescription] receive_notes(self, List[MixOutputEvents] output_events, PairingParameters pp)
Definition: wallet.py:168
zeth.core.mixer_client.compute_nullifier
bytes compute_nullifier(api.ZethNote zeth_note, OwnershipSecretKey spending_authority_ask)
Definition: mixer_client.py:718
zeth.core.wallet.Wallet.state_file
state_file
Definition: wallet.py:129
zeth.core.wallet.Wallet.wallet_dir
wallet_dir
Definition: wallet.py:126
zeth.core.utils.from_zeth_units
EtherValue from_zeth_units(int zeth_units)
Definition: utils.py:227
zeth.core.proto_utils
Definition: proto_utils.py:1
zeth.core.wallet.Wallet._decode_note_files_in_dir
Iterator[Tuple[int, str, EtherValue]] _decode_note_files_in_dir(self, str dir_name)
Definition: wallet.py:282
zeth.core.utils.EtherValue
Definition: utils.py:46
zeth.core.mixer_client.compute_commitment
bytes compute_commitment(api.ZethNote zeth_note, PairingParameters pp)
Definition: mixer_client.py:701
zeth.core.wallet.WalletState.next_block
next_block
Definition: wallet.py:77
zeth.core.zeth_address
Definition: zeth_address.py:1
zeth.core.wallet.Wallet._find_note_file
Optional[str] _find_note_file(self, str key)
Definition: wallet.py:294
zeth.core.wallet.Wallet._mark_note_spent
None _mark_note_spent(self, str nullifier_hex, str short_commit)
Definition: wallet.py:255
zeth.core.wallet.ZethNoteDescription.__init__
def __init__(self, ZethNote note, int address, bytes commitment)
Definition: wallet.py:40
zeth.core.proto_utils.zeth_note_from_json_dict
ZethNote zeth_note_from_json_dict(Dict[str, str] parsed_zeth_note)
Definition: proto_utils.py:37
zeth.core.proto_utils.zeth_note_to_json_dict
Dict[str, str] zeth_note_to_json_dict(ZethNote zeth_note_grpc_obj)
Definition: proto_utils.py:28
zeth.core.wallet.Wallet.username
username
Definition: wallet.py:125
zeth.core.wallet.Wallet.next_addr
next_addr
Definition: wallet.py:137