diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..b9dfef7 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length=82 diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..121bbc0 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,19 @@ +[MESSAGES CONTROL] +disable= + bad-continuation, + too-many-arguments, + missing-class-docstring, + missing-function-docstring, + too-many-locals, + too-many-statements, + too-few-public-methods, + wrong-import-order, + missing-module-docstring, + broad-except, + too-many-function-args, + duplicate-code, + +good-names=i,e,x,y,m,k,h,c,cm,rc,ek,ct,vk,sk,pk,X,Y,r,el,nf + +[REPORTS] +output-format=text diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..61430c4 --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ + +.PHONY: setup setup-dev dev check test syntax grpc test_contracts + +setup: + pip install --upgrade pip --progress-bar off + pip install -e . --progress-bar off + $(MAKE) grpc + python -c "from zeth.contracts import install_sol; \ + install_sol()" + +check: syntax test + +PROTOBUF_OUTPUT := \ + api/prover_pb2.py api/prover_pb2_grpc.py \ + api/pghr13_messages_pb2.py api/pghr13_messages_pb2_grpc.py \ + api/groth16_messages_pb2.py api/groth16_messages_pb2_grpc.py \ + api/ec_group_messages_pb2.py api/ec_group_messages_pb2_grpc.py \ + api/snark_messages_pb2.py api/snark_messages_pb2_grpc.py \ + api/zeth_messages_pb2.py api/zeth_messages_pb2_grpc.py + +api/%_pb2.py api/%_pb2_grpc.py: ../api/%.proto + python -m grpc_tools.protoc \ + -I.. --proto_path .. --python_out=. --grpc_python_out=. --mypy_out=. \ + api/$*.proto + +grpc: $(PROTOBUF_OUTPUT) + @# suppress "Nothing to do for ..." warning + @echo -n + +syntax: ${PROTOBUF_OUTPUT} + flake8 `git ls-files '**.py'` + mypy -p api + mypy -p zeth + mypy -p test + mypy -p test_commands + mypy -p commands + pylint zeth test test_commands commands + +test: ${PROTOBUF_OUTPUT} + python -m unittest + +test_contracts: ${PROTOBUF_OUTPUT} + python test/test_contract_base_mixer.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..de2864a --- /dev/null +++ b/README.md @@ -0,0 +1,308 @@ +# Python client to interact with the prover + +## Setup + +Ensure that the following are installed: + +- Python 3.7 (See `python --version`) +- [venv](https://docs.python.org/3/library/venv.html#module-venv) module. +- gcc + +Execute the following inside the `client` directory. +```console +$ python -m venv env +$ source env/bin/activate +(env)$ make setup +``` + +(It may also be necessary to install solc manually if the `py-solc-x` package +fails to find it. See the instructions below.) + +We assume all further commands described here are executed from within the +Python virtualenv. To enter the virtualenv from a new terminal, re-run +```console +$ source env/bin/activate +``` + +## Execute unit tests + +```console +(env)$ make check +``` + +## Execute testing client + +These are scripts that perform some predetermined transactions between a set of +users: Alice, Bob and Charlie. + +Test ether mixing: +```console +(env)$ test_ether_mixing.py [ZKSNARK] +``` + +Test ERC token mixing: +```console +(env)$ test_erc_token_mixing.py [ZKSNARK] +``` + +where `[ZKSNARK]` is the zksnark to use (must be the same as the one used on +the server). + +## Note on solc compiler installation + +Note that `make setup` will automatically install the solidity compiler in `$HOME/.solc` +(if required) and not in the python virtual environment. + +# The `zeth` command line interface + +The `zeth` command exposes Zeth operations via a command line interface. A +brief description is given in this section. More details are available via +`zeth --help`, and example usage can be seen in the [pyclient test +script](../scripts/test_zeth_cli). + +## Environment + +Depending on the operation being performed, the `zeth` client must: +- interact with an Ethereum RPC host, +- interact with the deployed Zeth contracts, +- request proofs and proof verification keys from `prover_server`, and +- access secret and public data for the current user + +Quite a lot of information must be given in order for the client to do this, +and the primary and auxiliary inputs to a Zeth operation are generally very +long. It can therefore be difficult to pass this information to the zeth +commands as command-line arguments. Thus, such data is stored in files with +default file names (which can be overridden on the zeth commands). + +The set of files required by Zeth for a single user to interact with a specific +deployment is described below. We recommend creating a directory for each +user/Zeth deployment, containing the following files. In this way, it is very +easy to setup one or more conceptual "users" and invoke `zeth` operations on +behalf of each of them to experiment with the system. + +- `eth-address` specifies an Ethereum address from which to transactions should + be funded. When running the testnet (see [top-level README](../README.md)), + addresses are created at startup and written to the console. One of these can + be copy-pasted into this file. +- `zeth-instance.json` contains the address and ABI for a single instance of + the zeth contract. This file is created by the deployment step below and + should be distributed to each client that will use this instance. +- `zeth-address.json` and `zeth-address.json.pub` hold the secret and public + parts of a ZethAddress. These can be generated with the `zeth gen-address` + command. `zeth-address.json.pub` holds the public address which can be shared + with other users, allowing them to privately transfer funds to this client. + The secret `zeth-address.json` should **not** be shared. + +Note that by default the `zeth` command will also create a `notes` +subdirectory to contain the set of notes owned by this user. These are also +specific to a particular Zeth deployment. + +Thereby, in the case of a Zeth user interacting with multiple Zeth deployments +(for example one for privately transferring Ether, and another for an ERC20 +token), a directory should be created for each deployment: + +``` + MyZethInstances/ + Ether/ + eth-address + zeth-instance.json + zeth-address.json + zeth-address.json.pub + notes/... + ERCToken1/ + eth-address + zeth-instance.json + zeth-address.json + zeth-address.json.pub + notes/... +``` + +`zeth` commands invoked inside `MyZethInstances/Ether` will target the Zeth +deployment that handles Ether. Similarly, commands executed inside +`MyZethInstances/ERCToken1` will target the deployment that handles the token +"ERCToken1". + +## Deployment + +Deployment compiles and deploys the contracts and initializes them with +appropriate data to create a new instance of the Zeth mixer. It requires only +an `eth-address` file mentioned above, where the address has sufficient funds. + +```console +# Create a clean directory for the deployer +(env)$ mkdir deployer +(env)$ cd deployer + +# Specify an eth-address file for an (unlocked) Ethereum account +(env)$ echo 0x.... > eth-address + +# Compile and deploy +(env)$ zeth deploy + +# Share the instance file with all clients +$ cp zeth-instance.json +``` + +## User setup + +To set up her client, Alice must setup all client files mentioned above: +```console +# Create a clean client directory +$ mkdir alice +$ cd alice + +# Specify an eth-address file for an (unlocked) Ethereum account +$ echo 0x.... > eth-address + +# Copy the instance file (received from the deployer) +$ cp zeth-instance.json + +# Generate new Zeth Address with secret (zeth-address.json) and +# public address (zeth-address.json.pub) +$ zeth gen-address + +# Share the public address with other users +$ cp zeth-address.json.pub +``` + +With these files in place, `zeth` commands invoked from inside this directory +can perform actions on behalf of Alice. We call this Alice's *client directory* +below, and assume that all commands are executed in a directory with these +files. + +## Receiving transactions + +The following command scans the blockchain for any new transactions which +generate Zeth notes indended for the public address `zeth-address.json.pub`: + +```console +# Check all new blocks for notes addressed to `zeth-address.json.pub`, +# storing them in the ./notes directory. +(env)$ zeth sync +``` + +Any notes found are stored in the `./notes` directory as individual files. +These files contain the secret data required to spend the note. + +```console +# List all notes received by this client +$ zeth ls-notes +``` +lists information about all known notes belonging to the current user. + +## Mix command + +The `zeth mix` command is used to interact with a deployed Zeth Mixer instance. +The command accepts the following information: + +**Input Notes.** Zeth notes owned by the current client, which should be visible +via `zeth ls-notes`. Either the integer "address" or the truncated commitment +value (8 hex chars) can be used to specify which notes to use as inputs. + +**Output Notes.** Given as pairs of Zeth public address and value, separated by +a comma `,`. The form of the public address is exactly as in the +`zeth-address.json.pub` file. That is, two 32 byte hex values separated by a +colon `:`. + +**Public Input.** Ether or ERC20 token value to deposit in the mixer. + +**Public Output.** Ether or ERC20 tokens value to be withdrawn from the mixer. + +Some examples are given below + +### Depositing funds + +A simple deposit consists of some public input (ether or tokens), and the +creation of Zeth notes. + +```console +# Deposit 10 ether from `eth-address`, creating Zeth notes owned by Alice +(env)$ zeth mix --out ,10 --vin 10 +``` +where `` is the contents of `zeth-address.json.pub`. + +### Privately send a ZethNote to another user + +To privately transfer value within the mixer, no public input / output is +required. Unspent notes (inputs) and destination addresses and output note +values are specified. + +```console +$ zeth ls-notes +b1a2feaf: value=200, addr=0 +eafe5f84: value=100, addr=2 + +$ zeth mix \ + --in eafe5f84 \ # "eafe5f84: value=100, addr=2" + --in 0 \ # "b1a2feaf: value=200, addr=0" + --out d77f...0e00:cc7c....7f76,120 \ # 120 to this addr + --out 3a43...fd3b:9fc8....b838,180 # 180 to this addr +``` + +### Withdrawing funds from the mixer + +Specify the note(s) to be withdrawn, and the total value as public output: +```console +$ zeth mix --in eafe5f84 --vout 100 +``` + +### A note on the `zeth mix` command + +As explained above, the `zeth mix` command can be used to deposit funds on the +mixer, transfer notes, and withdraw funds from the mixer. A single command can +perform all of these in one transaction, which greatly improves the privacy +level provided by Zeth. In fact, no exact information about the meaning of a +transaction is ever leaked to the an observant attacker. + +Here are a few examples of complex payments allowed by `zeth mix`: + +```console +$ zeth ls-notes +b1a2feaf: value=200, addr=0 +eafe5f84: value=100, addr=2 + +$ zeth mix \ + --in eafe5f84 \ # "eafe5f84: value=100, addr=2" + --vin 5 \ + --out d77f...0e00:cc7c....7f76,103 \ # 103 to this address (e.g. Bob) + --out 3a43...fd3b:9fc8....b838,2 # 2 to another addr (e.g. my refund) + +zeth mix \ + --in eafe5f84 \ # "eafe5f84: value=100, addr=2" + --out d77f...0e00:cc7c....7f76,98.5 \ # 98.5 to this address (e.g. Bob) + --vout 1.5 +``` + +### Async transactions + +The `mix` command broadcasts transactions to the Ethereum network and by default +output the transaction ID. Users can wait for these transactions to be accepted +into the blockchain by passing this ID to the `zeth sync` command via the +`--wait-tx` flag. This command waits for the transaction to be committed and +then searches for new notes. + +Alternatively, the `--wait` flag can be passed to the `mix` command to make it +wait and sync new notes before exiting. + +## Limitations - Note and Address management + +As proof-of-concept software, these tools are not suitable for use in a +production environment and have several functional limitations. Some of those +limitations are mentioned here. + +The `zeth` tool suite does not track which of the client's notes have been +spent by previous operations. In the presence of async transactions and +possible forks in the chain, such tracking logic would greatly increase the +complexity of the client tools and is considered out of scope for this +proof-of-concept. The user must manually track which notes have been spent (for +example by moving their files into a `spent` subdirectory where they will not +be seen by the wallet). + +All values that make up the Zeth secret address, and Zeth note data (required +to spend notes) are stored in plaintext. A fully secure client would encrypt +these to protect them from malicious entities that may gain access to the file +system. Such client-side security mechanisms are also beyond the scope of this +proof-of-concept implementation. + +Similarly, such address and note data is not automatically backed up or +otherwise protected by these tools. diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..b6e016d --- /dev/null +++ b/api/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ diff --git a/api/py.typed b/api/py.typed new file mode 100644 index 0000000..a0c5d77 --- /dev/null +++ b/api/py.typed @@ -0,0 +1,5 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +# Empty file, required for mypy. \ No newline at end of file diff --git a/commands/__init__.py b/commands/__init__.py new file mode 100644 index 0000000..b6e016d --- /dev/null +++ b/commands/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ diff --git a/commands/constants.py b/commands/constants.py new file mode 100644 index 0000000..a6e9abb --- /dev/null +++ b/commands/constants.py @@ -0,0 +1,18 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + + +""" +Constants and defaults specific to the CLI interface. +""" + +ETH_RPC_ENDPOINT_DEFAULT = "http://localhost:8545" +PROVER_SERVER_ENDPOINT_DEFAULT = "localhost:50051" + +ADDRESS_FILE_DEFAULT = "zeth-address.json" +INSTANCE_FILE_DEFAULT = "zeth-instance.json" +ETH_ADDRESS_DEFAULT = "eth-address" + +WALLET_DIR_DEFAULT = "./wallet" +WALLET_USERNAME = "zeth" diff --git a/commands/py.typed b/commands/py.typed new file mode 100644 index 0000000..a0c5d77 --- /dev/null +++ b/commands/py.typed @@ -0,0 +1,5 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +# Empty file, required for mypy. \ No newline at end of file diff --git a/commands/utils.py b/commands/utils.py new file mode 100644 index 0000000..61c7655 --- /dev/null +++ b/commands/utils.py @@ -0,0 +1,303 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from __future__ import annotations +from commands.constants import WALLET_USERNAME, ETH_ADDRESS_DEFAULT +from zeth.zeth_address import ZethAddressPub, ZethAddressPriv, ZethAddress +from zeth.contracts import \ + InstanceDescription, get_block_number, get_mix_results, compile_files +from zeth.mixer_client import MixerClient +from zeth.utils import \ + open_web3, short_commitment, EtherValue, get_zeth_dir, from_zeth_units +from zeth.wallet import ZethNoteDescription, Wallet +from click import ClickException +import json +from os.path import exists, join +from typing import Dict, Tuple, Optional, Callable, Any +from web3 import Web3 # type: ignore + + +class ClientConfig: + """ + Context for users of these client tools + """ + def __init__( + self, + eth_rpc_endpoint: str, + prover_server_endpoint: str, + instance_file: str, + address_file: str, + wallet_dir: str): + self.eth_rpc_endpoint = eth_rpc_endpoint + self.prover_server_endpoint = prover_server_endpoint + self.instance_file = instance_file + self.address_file = address_file + self.wallet_dir = wallet_dir + + +def open_web3_from_ctx(ctx: ClientConfig) -> Any: + return open_web3(ctx.eth_rpc_endpoint) + + +class MixerDescription: + """ + Holds an InstanceDescription for the mixer contract, and optionally an + InstanceDescription for the token contract. + """ + def __init__( + self, + mixer: InstanceDescription, + token: Optional[InstanceDescription]): + self.mixer = mixer + self.token = token + + def to_json(self) -> str: + json_dict = { + "mixer": self.mixer.to_json_dict() + } + if self.token: + json_dict["token"] = self.token.to_json_dict() + return json.dumps(json_dict) + + @staticmethod + def from_json(json_str: str) -> MixerDescription: + json_dict = json.loads(json_str) + mixer = InstanceDescription.from_json_dict(json_dict["mixer"]) + token_dict = json_dict.get("token", None) + token = InstanceDescription.from_json_dict(token_dict) \ + if token_dict else None + return MixerDescription(mixer, token) + + +def get_erc20_abi() -> Dict[str, Any]: + zeth_dir = get_zeth_dir() + openzeppelin_dir = join( + zeth_dir, "zeth_contracts", "node_modules", "openzeppelin-solidity") + ierc20_path = join( + openzeppelin_dir, "contracts", "token", "ERC20", "IERC20.sol") + compiled_sol = compile_files([ierc20_path]) + erc20_interface = compiled_sol[ierc20_path + ":IERC20"] + return erc20_interface["abi"] + + +def get_erc20_instance_description(token_address: str) -> InstanceDescription: + return InstanceDescription(token_address, get_erc20_abi()) + + +def write_mixer_description( + mixer_desc_file: str, + mixer_desc: MixerDescription) -> None: + """ + Write the mixer (and token) instance information + """ + with open(mixer_desc_file, "w") as instance_f: + instance_f.write(mixer_desc.to_json()) + + +def load_mixer_description(mixer_description_file: str) -> MixerDescription: + """ + Return mixer and token (if present) contract instances + """ + with open(mixer_description_file, "r") as desc_f: + return MixerDescription.from_json(desc_f.read()) + + +def load_mixer_description_from_ctx(ctx: ClientConfig) -> MixerDescription: + return load_mixer_description(ctx.instance_file) + + +def get_zeth_address_file(ctx: ClientConfig) -> str: + return ctx.address_file + + +def load_zeth_address_public(ctx: ClientConfig) -> ZethAddressPub: + """ + Load a ZethAddressPub from a key file. + """ + secret_key_file = get_zeth_address_file(ctx) + pub_addr_file = pub_address_file(secret_key_file) + with open(pub_addr_file, "r") as pub_addr_f: + return ZethAddressPub.parse(pub_addr_f.read()) + + +def write_zeth_address_public( + pub_addr: ZethAddressPub, pub_addr_file: str) -> None: + """ + Write a ZethAddressPub to a file + """ + with open(pub_addr_file, "w") as pub_addr_f: + pub_addr_f.write(str(pub_addr)) + + +def load_zeth_address_secret(ctx: ClientConfig) -> ZethAddressPriv: + """ + Read ZethAddressPriv + """ + addr_file = get_zeth_address_file(ctx) + with open(addr_file, "r") as addr_f: + return ZethAddressPriv.from_json(addr_f.read()) + + +def write_zeth_address_secret( + secret_addr: ZethAddressPriv, addr_file: str) -> None: + """ + Write ZethAddressPriv to file + """ + with open(addr_file, "w") as addr_f: + addr_f.write(secret_addr.to_json()) + + +def load_zeth_address(ctx: ClientConfig) -> ZethAddress: + """ + Load a ZethAddress secret from a file, and the associated public address, + and return as a ZethAddress. + """ + return ZethAddress.from_secret_public( + load_zeth_address_secret(ctx), + load_zeth_address_public(ctx)) + + +def open_wallet( + mixer_instance: Any, + js_secret: ZethAddressPriv, + ctx: ClientConfig) -> Wallet: + """ + Load a wallet using a secret key. + """ + wallet_dir = ctx.wallet_dir + return Wallet(mixer_instance, WALLET_USERNAME, wallet_dir, js_secret) + + +def do_sync( + web3: Any, + wallet: Wallet, + wait_tx: Optional[str], + callback: Optional[Callable[[ZethNoteDescription], None]] = None) -> int: + """ + Implementation of sync, reused by several commands. Returns the + block_number synced to. Also updates and saves the MerkleTree. + """ + def _do_sync() -> int: + wallet_next_block = wallet.get_next_block() + chain_block_number: int = get_block_number(web3) + + if chain_block_number >= wallet_next_block: + new_merkle_root: Optional[bytes] = None + + print(f"SYNCHING blocks ({wallet_next_block} - {chain_block_number})") + mixer_instance = wallet.mixer_instance + for mix_result in get_mix_results( + web3, mixer_instance, wallet_next_block, chain_block_number): + new_merkle_root = mix_result.new_merkle_root + for note_desc in wallet.receive_notes(mix_result.output_events): + if callback: + callback(note_desc) + + spent_commits = wallet.mark_nullifiers_used(mix_result.nullifiers) + for commit in spent_commits: + print(f" SPENT: {commit}") + + wallet.update_and_save_state(next_block=chain_block_number + 1) + + # Check merkle root and save the updated tree + if new_merkle_root: + our_merkle_root = wallet.merkle_tree.get_root() + assert new_merkle_root == our_merkle_root + + return chain_block_number + + # Do a sync upfront (it would be a waste of time to wait for a tx before + # syncing, as it can take time to traverse all blocks). Then wait for a tx + # if requested, and sync again. + + if wait_tx: + _do_sync() + tx_receipt = web3.eth.waitForTransactionReceipt(wait_tx, 10000) + gas_used = tx_receipt.gasUsed + status = tx_receipt.status + print(f"{wait_tx[0:8]}: gasUsed={gas_used}, status={status}") + + return _do_sync() + + +def pub_address_file(addr_file: str) -> str: + """ + The name of a public address file, given the secret address file. + """ + return addr_file + ".pub" + + +def find_pub_address_file(base_file: str) -> str: + """ + Given a file name, which could point to a private or public key file, guess + at the name of the public key file. + """ + pub_addr_file = pub_address_file(base_file) + if exists(pub_addr_file): + return pub_addr_file + if exists(base_file): + return base_file + + raise ClickException(f"No public key file {pub_addr_file} or {base_file}") + + +def create_mixer_client(ctx: ClientConfig) -> MixerClient: + """ + Create a MixerClient for an existing deployment. + """ + web3 = open_web3_from_ctx(ctx) + mixer_desc = load_mixer_description_from_ctx(ctx) + mixer_instance = mixer_desc.mixer.instantiate(web3) + return MixerClient.open(web3, ctx.prover_server_endpoint, mixer_instance) + + +def create_zeth_client_and_mixer_desc( + ctx: ClientConfig) -> Tuple[MixerClient, MixerDescription]: + """ + Create a MixerClient and MixerDescription object, for an existing deployment. + """ + web3 = open_web3_from_ctx(ctx) + mixer_desc = load_mixer_description_from_ctx(ctx) + mixer_instance = mixer_desc.mixer.instantiate(web3) + zeth_client = MixerClient.open( + web3, ctx.prover_server_endpoint, mixer_instance) + return (zeth_client, mixer_desc) + + +def zeth_note_short(note_desc: ZethNoteDescription) -> str: + """ + Generate a short human-readable description of a commitment. + """ + value = from_zeth_units(int(note_desc.note.value, 16)).ether() + cm = short_commitment(note_desc.commitment) + return f"{cm}: value={value} ETH, addr={note_desc.address}" + + +def zeth_note_short_print(note_desc: ZethNoteDescription) -> None: + print(f" NEW NOTE: {zeth_note_short(note_desc)}") + + +def parse_output(output_str: str) -> Tuple[ZethAddressPub, EtherValue]: + """ + Parse a string of the form "," to an output + specification. + """ + parts = output_str.split(",") + if len(parts) != 2: + raise ClickException(f"invalid output spec: {output_str}") + return (ZethAddressPub.parse(parts[0]), EtherValue(parts[1])) + + +def load_eth_address(eth_addr: Optional[str]) -> str: + """ + Given an --eth-addr command line param, either parse the address, load from + the file, or use a default file name. + """ + eth_addr = eth_addr or ETH_ADDRESS_DEFAULT + if eth_addr.startswith("0x"): + return Web3.toChecksumAddress(eth_addr) + if exists(eth_addr): + with open(eth_addr, "r") as eth_addr_f: + return Web3.toChecksumAddress(eth_addr_f.read().rstrip()) + raise ClickException(f"could find file or parse eth address: {eth_addr}") diff --git a/commands/zeth b/commands/zeth new file mode 100644 index 0000000..b39fb00 --- /dev/null +++ b/commands/zeth @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from commands.constants import \ + PROVER_SERVER_ENDPOINT_DEFAULT, INSTANCE_FILE_DEFAULT, \ + ADDRESS_FILE_DEFAULT, WALLET_DIR_DEFAULT, ETH_RPC_ENDPOINT_DEFAULT +from commands.utils import ClientConfig +from commands.zeth_deploy import deploy +from commands.zeth_gen_address import gen_address +from commands.zeth_sync import sync +from commands.zeth_mix import mix +from commands.zeth_ls_notes import ls_notes +from commands.zeth_ls_commits import ls_commits +from commands.zeth_token_approve import token_approve +from click import group, command, option, pass_context, ClickException, Context +from click_default_group import DefaultGroup +from typing import Optional, Any + + +@command() +@pass_context +def help(ctx: Context) -> None: + """ + Print help and exit + """ + # Note, this command is implemented to ensure that an error is raised if no + # subcommand is specified (which also catches errors in scripts). + print(ctx.parent.get_help()) + raise ClickException("no command specified") + + +@group(cls=DefaultGroup, default_if_no_args=True, default="help") +@option( + "--eth-rpc", + default=ETH_RPC_ENDPOINT_DEFAULT, + help=f"Ethereum rpc end-point") +@option( + "--prover-server", + default=PROVER_SERVER_ENDPOINT_DEFAULT, + help=f"Prover server endpoint (default={PROVER_SERVER_ENDPOINT_DEFAULT})") +@option( + "--instance-file", + default=INSTANCE_FILE_DEFAULT, + help=f"Instance file (default={INSTANCE_FILE_DEFAULT})") +@option( + "--address-file", + default=ADDRESS_FILE_DEFAULT, + help=f"Instance file (default={ADDRESS_FILE_DEFAULT})") +@option( + "--wallet-dir", + default=WALLET_DIR_DEFAULT, + help=f"Wallet directory (default={WALLET_DIR_DEFAULT})") +@pass_context +def zeth( + ctx: Context, + eth_rpc: Optional[str], + prover_server: str, + instance_file: str, + address_file: str, + wallet_dir: str) -> None: + if ctx.invoked_subcommand == "help": + ctx.invoke(help) + ctx.ensure_object(dict) + ctx.obj = ClientConfig( + eth_rpc_endpoint=eth_rpc, + prover_server_endpoint=prover_server, + instance_file=instance_file, + address_file=address_file, + wallet_dir=wallet_dir) + + +zeth.add_command(deploy) +zeth.add_command(gen_address) +zeth.add_command(sync) +zeth.add_command(mix) +zeth.add_command(ls_notes) +zeth.add_command(ls_commits) +zeth.add_command(token_approve) +zeth.add_command(help) + + +if __name__ == "__main__": + zeth() # pylint: disable=no-value-for-parameter diff --git a/commands/zeth_deploy.py b/commands/zeth_deploy.py new file mode 100644 index 0000000..054d6cc --- /dev/null +++ b/commands/zeth_deploy.py @@ -0,0 +1,53 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from commands.constants import INSTANCE_FILE_DEFAULT +from commands.utils import \ + open_web3_from_ctx, get_erc20_instance_description, load_eth_address, \ + write_mixer_description, MixerDescription +from zeth.mixer_client import MixerClient +from zeth.utils import EtherValue +from click import Context, command, option, pass_context +from typing import Optional + + +@command() +@option("--eth-addr", help="Sender eth address or address filename") +@option( + "--instance-out", + default=INSTANCE_FILE_DEFAULT, + help=f"File to write deployment address to (default={INSTANCE_FILE_DEFAULT})") +@option("--token-address", help="Address of token contract (if used)") +@option("--deploy-gas", help="Maximum gas, in Wei") +@pass_context +def deploy( + ctx: Context, + eth_addr: Optional[str], + instance_out: str, + token_address: str, + deploy_gas: str) -> None: + """ + Deploy the zeth contracts and record the instantiation details. + """ + eth_address = load_eth_address(eth_addr) + client_ctx = ctx.obj + web3 = open_web3_from_ctx(client_ctx) + deploy_gas_value = EtherValue(deploy_gas, 'wei') if deploy_gas else None + + print(f"deploy: eth_address={eth_address}") + print(f"deploy: instance_out={instance_out}") + print(f"deploy: token_address={token_address}") + + token_instance_desc = get_erc20_instance_description(token_address) \ + if token_address else None + + _zeth_client, mixer_instance_desc = MixerClient.deploy( + web3, + client_ctx.prover_server_endpoint, + eth_address, + token_address, + deploy_gas_value) + + mixer_desc = MixerDescription(mixer_instance_desc, token_instance_desc) + write_mixer_description(instance_out, mixer_desc) diff --git a/commands/zeth_gen_address.py b/commands/zeth_gen_address.py new file mode 100644 index 0000000..dcae8c1 --- /dev/null +++ b/commands/zeth_gen_address.py @@ -0,0 +1,31 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from zeth.zeth_address import generate_zeth_address +from commands.utils import get_zeth_address_file, pub_address_file, \ + write_zeth_address_secret, write_zeth_address_public +from click import command, pass_context, ClickException, Context +from os.path import exists + + +@command() +@pass_context +def gen_address(ctx: Context) -> None: + """ + Generate a new Zeth secret key and public address + """ + client_ctx = ctx.obj + addr_file = get_zeth_address_file(client_ctx) + if exists(addr_file): + raise ClickException(f"ZethAddress file {addr_file} exists") + + pub_addr_file = pub_address_file(addr_file) + if exists(pub_addr_file): + raise ClickException(f"ZethAddress pub file {pub_addr_file} exists") + + zeth_address = generate_zeth_address() + write_zeth_address_secret(zeth_address.addr_sk, addr_file) + print(f"ZethAddress Secret key written to {addr_file}") + write_zeth_address_public(zeth_address.addr_pk, pub_addr_file) + print(f"Public ZethAddress written to {pub_addr_file}") diff --git a/commands/zeth_ls_commits.py b/commands/zeth_ls_commits.py new file mode 100644 index 0000000..d906271 --- /dev/null +++ b/commands/zeth_ls_commits.py @@ -0,0 +1,24 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from commands.utils import \ + create_zeth_client_and_mixer_desc, load_zeth_address, open_wallet +from zeth.utils import short_commitment +from click import Context, command, pass_context + + +@command() +@pass_context +def ls_commits(ctx: Context) -> None: + """ + List all commitments in the joinsplit contract + """ + client_ctx = ctx.obj + zeth_client, _mixer_desc = create_zeth_client_and_mixer_desc(client_ctx) + zeth_address = load_zeth_address(client_ctx) + wallet = open_wallet( + zeth_client.mixer_instance, zeth_address.addr_sk, client_ctx) + print("COMMITMENTS:") + for commit in wallet.merkle_tree.get_leaves(): + print(f" {short_commitment(commit)}") diff --git a/commands/zeth_ls_notes.py b/commands/zeth_ls_notes.py new file mode 100644 index 0000000..1178669 --- /dev/null +++ b/commands/zeth_ls_notes.py @@ -0,0 +1,39 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from commands.utils import open_web3_from_ctx, load_zeth_address_secret, \ + open_wallet, load_mixer_description_from_ctx +from zeth.utils import EtherValue +from click import Context, command, option, pass_context + + +@command() +@option("--balance", is_flag=True, help="Show total balance") +@option("--spent", is_flag=True, help="Show spent notes") +@pass_context +def ls_notes(ctx: Context, balance: bool, spent: bool) -> None: + """ + List the set of notes owned by this wallet + """ + client_ctx = ctx.obj + web3 = open_web3_from_ctx(client_ctx) + mixer_desc = load_mixer_description_from_ctx(client_ctx) + mixer_instance = mixer_desc.mixer.instantiate(web3) + js_secret = load_zeth_address_secret(client_ctx) + wallet = open_wallet(mixer_instance, js_secret, client_ctx) + + total = EtherValue(0) + for addr, short_commit, value in wallet.note_summaries(): + print(f"{short_commit}: value={value.ether()}, addr={addr}") + total = total + value + + if balance: + print(f"TOTAL BALANCE: {total.ether()}") + + if not spent: + return + + print("SPENT NOTES:") + for addr, short_commit, value in wallet.spent_note_summaries(): + print(f"{short_commit}: value={value.ether()}, addr={addr}") diff --git a/commands/zeth_mix.py b/commands/zeth_mix.py new file mode 100644 index 0000000..44ee6a7 --- /dev/null +++ b/commands/zeth_mix.py @@ -0,0 +1,83 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from commands.utils import create_zeth_client_and_mixer_desc, \ + load_zeth_address, open_wallet, parse_output, do_sync, load_eth_address +from zeth.constants import JS_INPUTS, JS_OUTPUTS +from zeth.mixer_client import ZethAddressPub +from zeth.utils import EtherValue, from_zeth_units +from api.zeth_messages_pb2 import ZethNote +from click import command, option, pass_context, ClickException, Context +from typing import List, Tuple, Optional + + +@command() +@option("--vin", default="0", help="public in value") +@option("--vout", default="0", help="public out value") +@option("--in", "input_notes", multiple=True) +@option("--out", "output_specs", multiple=True, help=",") +@option("--eth-addr", help="Sender eth address or address filename") +@option("--wait", is_flag=True) +@pass_context +def mix( + ctx: Context, + vin: str, + vout: str, + input_notes: List[str], + output_specs: List[str], + eth_addr: Optional[str], + wait: bool) -> None: + """ + Generic mix function + """ + # Some sanity checks + if len(input_notes) > JS_INPUTS: + raise ClickException(f"too many inputs (max {JS_INPUTS})") + if len(output_specs) > JS_OUTPUTS: + raise ClickException(f"too many outputs (max {JS_OUTPUTS})") + + print(f"vin = {vin}") + print(f"vout = {vout}") + + vin_pub = EtherValue(vin) + vout_pub = EtherValue(vout) + client_ctx = ctx.obj + zeth_client, mixer_desc = create_zeth_client_and_mixer_desc(client_ctx) + zeth_address = load_zeth_address(client_ctx) + wallet = open_wallet( + zeth_client.mixer_instance, zeth_address.addr_sk, client_ctx) + + inputs: List[Tuple[int, ZethNote]] = [ + wallet.find_note(note_id).as_input() for note_id in input_notes] + outputs: List[Tuple[ZethAddressPub, EtherValue]] = [ + parse_output(out_spec) for out_spec in output_specs] + + # Compute input and output value total and check that they match + input_note_sum = from_zeth_units( + sum([int(note.value, 16) for _, note in inputs])) + output_note_sum = sum([value for _, value in outputs], EtherValue(0)) + if vin_pub + input_note_sum != vout_pub + output_note_sum: + raise ClickException("input and output value mismatch") + + eth_address = load_eth_address(eth_addr) + + # If instance uses an ERC20 token, tx_value can be 0 not default vin_pub. + tx_value: Optional[EtherValue] = None + if mixer_desc.token: + tx_value = EtherValue(0) + + tx_hash = zeth_client.joinsplit( + wallet.merkle_tree, + zeth_address.ownership_keypair(), + eth_address, + inputs, + outputs, + vin_pub, + vout_pub, + tx_value) + + if wait: + do_sync(zeth_client.web3, wallet, tx_hash) + else: + print(tx_hash) diff --git a/commands/zeth_sync.py b/commands/zeth_sync.py new file mode 100644 index 0000000..3bbc2b2 --- /dev/null +++ b/commands/zeth_sync.py @@ -0,0 +1,25 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from commands.utils import open_web3_from_ctx, load_zeth_address_secret, \ + open_wallet, do_sync, load_mixer_description_from_ctx, zeth_note_short_print +from click import command, option, pass_context, Context +from typing import Optional + + +@command() +@option("--wait-tx", help="Wait for tx hash") +@pass_context +def sync(ctx: Context, wait_tx: Optional[str]) -> None: + """ + Attempt to retrieve new notes for the key in + """ + client_ctx = ctx.obj + web3 = open_web3_from_ctx(client_ctx) + mixer_desc = load_mixer_description_from_ctx(client_ctx) + mixer_instance = mixer_desc.mixer.instantiate(web3) + js_secret = load_zeth_address_secret(client_ctx) + wallet = open_wallet(mixer_instance, js_secret, client_ctx) + chain_block_number = do_sync(web3, wallet, wait_tx, zeth_note_short_print) + print(f"SYNCED to {chain_block_number}") diff --git a/commands/zeth_token_approve.py b/commands/zeth_token_approve.py new file mode 100644 index 0000000..6954960 --- /dev/null +++ b/commands/zeth_token_approve.py @@ -0,0 +1,35 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from commands.utils import load_eth_address, open_web3_from_ctx, \ + load_mixer_description_from_ctx, EtherValue +from click import command, argument, option, pass_context, ClickException, Context + + +@command() +@argument("tokens") +@option("--eth-addr", help="Sender eth address or address filename") +@option("--wait", is_flag=True, help="Wait for transaction to complete") +@pass_context +def token_approve(ctx: Context, tokens: str, eth_addr: str, wait: bool) -> None: + """ + Approve the mixer to spend some amount of tokens + """ + approve_value = EtherValue(tokens) + eth_addr = load_eth_address(eth_addr) + client_ctx = ctx.obj + web3 = open_web3_from_ctx(client_ctx) + mixer_desc = load_mixer_description_from_ctx(client_ctx) + if not mixer_desc.token: + raise ClickException("no token for mixer {mixer_desc.mixer.address}") + + token_instance = mixer_desc.token.instantiate(web3) + tx_hash = token_instance.functions.approve( + mixer_desc.mixer.address, + approve_value.wei).transact({'from': eth_addr}) + + if wait: + web3.eth.waitForTransactionReceipt(tx_hash) # pylint: disable=no-member + else: + print(tx_hash.hex()) diff --git a/multiple_test.sh b/multiple_test.sh new file mode 100644 index 0000000..24f7545 --- /dev/null +++ b/multiple_test.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Script to run 50 integration tests + +NOW=`date '+%F_%H:%M:%S'`; +filename="test_result_$NOW" + +echo "Test started\n" >> $filename + +for i in `seq 1 50`; +do + python testEtherMixing.py PGHR13 + ret=$? + + if [ $ret -ne 0 ]; then + echo "ETH test $i failed\n" >> $filename + fi + + python testERCTokenMixing.py PGHR13 + ret=$? + + if [ $ret -ne 0 ]; then + echo "Token test $i failed\n" >> $filename + fi + +echo "Test finished.\n" >> $filename +done diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..7b7a46b --- /dev/null +++ b/mypy.ini @@ -0,0 +1,19 @@ +[mypy] +strict_optional=True +disallow_untyped_calls=True +disallow_untyped_defs=True +disallow_incomplete_defs=True +check_untyped_defs=True +scripts_are_modules = True + +[mypy-grpc.*] +ignore_missing_imports = True + +[mypy-solcx] +ignore_missing_imports = True + +[mypy-api.*] +disallow_untyped_defs=False + +[mypy-cryptography.*] +ignore_missing_imports = True diff --git a/py.typed b/py.typed new file mode 100644 index 0000000..a0c5d77 --- /dev/null +++ b/py.typed @@ -0,0 +1,5 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +# Empty file, required for mypy. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9c558e3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +. diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b16262c --- /dev/null +++ b/setup.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +import sys +from setuptools import find_packages +from distutils.core import setup + +if not hasattr(sys, 'base_prefix') or sys.base_prefix == sys.prefix: + print("ERROR: This is not production software, install inside a venv") + sys.exit(1) + +if sys.version_info < (3, 7): + print("ERROR: requires python >=3.7") + sys.exit(1) + +setup( + name='zeth', + version='0.1', + description='Interface to zeth operations', + packages=find_packages(), + install_requires=[ + "mypy==0.720", + "mypy-protobuf==1.16", + "flake8==3.7.8", + "pylint==2.4.3", + "click==7.0", + "click-default-group==1.2", + "attrdict==2.0.1", + "certifi==2018.11.29", + "chardet==3.0.4", + "cytoolz==0.9.0.1", + "eth-abi==1.3.0", + "eth-account==0.3.0", + "eth-hash==0.2.0", + "eth-keyfile==0.5.1", + "eth-keys==0.2.1", + "eth-rlp==0.1.2", + "eth-typing==2.1.0", + "eth-utils==1.4.1", + "grpcio==1.24", + "grpcio-tools==1.24", + "hexbytes==0.1.0", + "idna==2.8", + "lru-dict==1.1.6", + "parsimonious==0.8.1", + "protobuf==3.6.1", + "py_ecc==1.7.1", + "py-solc-x==0.7.0", + "pycryptodome==3.9.0", + "cryptography==2.9", + "requests==2.21.0", + "rlp==1.1.0", + "semantic-version==2.8.4", + "six==1.12.0", + "toolz==0.9.0", + "urllib3==1.24.2", + "web3==4.8.2", + "websockets==6.0", + ], + scripts=[ + "test_commands/test_ether_mixing.py", + "test_commands/test_erc_token_mixing.py", + "test_commands/test_merkle_tree_contract.py", + "commands/zeth", + ] +) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..6fb9229 --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ diff --git a/test/test_contract_base_mixer.py b/test/test_contract_base_mixer.py new file mode 100644 index 0000000..f2300e1 --- /dev/null +++ b/test/test_contract_base_mixer.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from zeth.constants import \ + JS_INPUTS, JS_OUTPUTS, PUBLIC_VALUE_LENGTH, ZETH_PUBLIC_UNIT_VALUE +from zeth.mixer_client import MixerClient +from typing import Any +import test_commands.mock as mock + + +# The UNPACKED_PRIMARY_INPUTS variable represents a dummy primary input, +# it is structured as follows, +UNPACKED_PRIMARY_INPUTS = [ + 0, # rt + 24, # nf_0 = "0...01 1000" + 33, # nf_1 = "0...010 0001" + 1, # cm_0 = "0...01" + 2, # cm_1 = "0...010" + 2**PUBLIC_VALUE_LENGTH - 1, # v_in = "1...1" + 0, # v_out = "0...0" + 47, # h_sig = "0...010 1111" + 50, # htag_0 = "0...011 0010" + 59 # htag_1 = "0...011 1011" +] +# The values were set so that the RESIDUAL_BITS are easily distinguishable. + +# PACKED_PRIMARY_INPUTS = +# rt || {nf}_1,2 || {cm}_1,2 || h_sig || {h}_1,2 || RESIDUAL_BITS +PACKED_PRIMARY_INPUTS = [ + 0, # root + 1, # cm_0 + 2, # cm_1 + 3, # nf_0 + 4, # nf_1 + 5, # h_sig + 6, # h_0 + 7, # h_1 + 11150372599265311570163396226516866165665875] \ + # pylint: disable=no-member,invalid-name + +# RESIDUAL_BITS = +# v_in || v_out || h_sig || {nf}_1,2 || {h}_1,2 +# We set dummy values for all variables. The residual_bits are as follows: +# RESIDUAL_BITS = 713623846352979940490457358497079434602616037, or in bits +# 1-4: 00000000 00000000 00000000 00000000 +# 5-8: 00000000 00000000 00000000 00000000 +# 9-12: 00000000 00000000 00000000 00000000 +# 13-16: 00000000 00000000 01111111 11111111 +# 17-20: 11111111 11111111 11111111 11111111 +# 21-24: 11111111 11111111 10000000 00000000 +# 25-28: 00000000 00000000 00000000 00000000 +# 29-32: 00000000 00000000 01110000 01010011 +# This corresponds to +# v_in = "0xFFFFFFFFFFFFFFFF" = 2**PUBLIC_VALUE_LENGTH - 1 +# v_out = "0x0000000000000000" = 0 +# h_sig = "111" = 7 +# nf_0 = "000" = 0 +# nf_1 = "001" = 1 +# h_0 = "010" = 2 +# h_1 = "011" = 3 +RESIDUAL_BITS = [ + 2**PUBLIC_VALUE_LENGTH - 1, # v_in + 0, # v_out + 7, # h_sig + 0, # nf_0 + 1, # nf_1 + 2, # h_0 + 3 # h_1 + ] # pylint: disable=no-member,invalid-name + + +def test_assemble_nullifiers(mixer_instance: Any) -> int: + # Test retrieving nullifiers + print("--- testing ", "test_assemble_nullifiers") + for i in range(JS_INPUTS): + res = mixer_instance.functions.\ + assemble_nullifier(i, PACKED_PRIMARY_INPUTS).call() + val = int.from_bytes(res, byteorder="big") + if val != UNPACKED_PRIMARY_INPUTS[1+i]: + print("ERROR: extracted wrong nullifier") + print("expected:", UNPACKED_PRIMARY_INPUTS[1+i], i) + print("got:", val, i) + return 1 + return 0 + + +def test_assemble_commitments(mixer_instance: Any) -> int: + # Test retrieving commitments + print("--- testing ", "test_assemble_commitments") + for i in range(JS_OUTPUTS): + res = mixer_instance.functions.\ + assemble_commitment(i, PACKED_PRIMARY_INPUTS).call() + val = int.from_bytes(res, byteorder="big") + if val != UNPACKED_PRIMARY_INPUTS[1 + JS_INPUTS + i]: + print("ERROR: extracted wrong commitment") + print("expected:", UNPACKED_PRIMARY_INPUTS[1 + JS_INPUTS + i], i) + print("got:", val, i) + return 1 + return 0 + + +def test_assemble_hsig(mixer_instance: Any) -> Any: + # Test retrieving commitments + print("--- testing ", "test_assemble_hsig") + res = mixer_instance.functions.\ + assemble_hsig(PACKED_PRIMARY_INPUTS).call() + hsig = int.from_bytes(res, byteorder="big") + if hsig != UNPACKED_PRIMARY_INPUTS[JS_INPUTS + JS_OUTPUTS + 3]: + print("ERROR: extracted wrong public values") + print("expected:", UNPACKED_PRIMARY_INPUTS[JS_INPUTS + JS_OUTPUTS + 3]) + print("got:", hsig) + return 1 + return 0 + + +def test_assemble_vpub(mixer_instance: Any) -> int: + # Test retrieving commitments + print("--- testing ", "test_assemble_vpub") + v_in, v_out = mixer_instance.functions.assemble_public_values( + PACKED_PRIMARY_INPUTS).call() + v_in_expect = UNPACKED_PRIMARY_INPUTS[JS_INPUTS + JS_OUTPUTS + 1] \ + * ZETH_PUBLIC_UNIT_VALUE + v_out_expect = UNPACKED_PRIMARY_INPUTS[JS_INPUTS + JS_OUTPUTS + 2] \ + * ZETH_PUBLIC_UNIT_VALUE + + if v_in != v_in_expect or v_out != v_out_expect: + print("ERROR: extracted wrong public values") + print(f"expected: {(v_in_expect, v_out_expect)}") + print(f"actual : {(v_in, v_out)}") + return 1 + return 0 + + +def main() -> None: + print("-------------------- Evaluating BaseMixer.sol --------------------") + + web3, eth = mock.open_test_web3() + + # Ethereum addresses + deployer_eth_address = eth.accounts[0] + + zeth_client, _ = MixerClient.deploy( + web3, mock.TEST_PROVER_SERVER_ENDPOINT, deployer_eth_address) + + mixer_instance = zeth_client.mixer_instance + + # We can now call the instance and test its functions. + print("[INFO] 4. Running tests") + result = 0 + result += test_assemble_commitments(mixer_instance) + result += test_assemble_nullifiers(mixer_instance) + result += test_assemble_vpub(mixer_instance) + result += test_assemble_hsig(mixer_instance) + # We do not re-assemble of h_is in the contract + + if result == 0: + print("base_mixer tests PASS\n") + + +if __name__ == '__main__': + main() diff --git a/test/test_contracts.py b/test/test_contracts.py new file mode 100644 index 0000000..d6ff459 --- /dev/null +++ b/test/test_contracts.py @@ -0,0 +1,51 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +""" +Tests for zeth.contracts module +""" + +from zeth.contracts import MixParameters +from zeth.encryption import generate_encryption_keypair, encrypt +from zeth.signing import gen_signing_keypair, sign, encode_vk_to_bytes +from zeth.constants import NOTE_LENGTH_BYTES +from unittest import TestCase +from secrets import token_bytes + + +class TestContracts(TestCase): + + def test_mix_parameters(self) -> None: + + ext_proof = { + "a": ["1234", "2345"], + "b": [["3456", "4567"], ["5678", "6789"]], + "c": ["789a", "89ab"], + "inputs": [ + "9abc", + "abcd", + "bcde", + "cdef", + ], + } + sig_keypair = gen_signing_keypair() + sig_vk = sig_keypair.vk + sig = sign(sig_keypair.sk, bytes.fromhex("00112233")) + receiver_enc_keypair = generate_encryption_keypair() + ciphertexts = [ + encrypt(token_bytes(NOTE_LENGTH_BYTES), receiver_enc_keypair.k_pk), + encrypt(token_bytes(NOTE_LENGTH_BYTES), receiver_enc_keypair.k_pk), + ] + + mix_params = MixParameters(ext_proof, sig_vk, sig, ciphertexts) + + mix_params_json = mix_params.to_json() + mix_params_2 = MixParameters.from_json(mix_params_json) + + self.assertEqual(mix_params.extended_proof, mix_params_2.extended_proof) + self.assertEqual( + encode_vk_to_bytes(mix_params.signature_vk), + encode_vk_to_bytes(mix_params_2.signature_vk)) + self.assertEqual(mix_params.signature, mix_params_2.signature) + self.assertEqual(mix_params.ciphertexts, mix_params_2.ciphertexts) diff --git a/test/test_encryption.py b/test/test_encryption.py new file mode 100644 index 0000000..cea0aaf --- /dev/null +++ b/test/test_encryption.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +import zeth.constants as constants +import zeth.encryption as encryption +from unittest import TestCase + + +_TEST_SECRET_KEY_BYTES = bytes.fromhex( + "10c78d9b7cca67e76c528232ff6fb69c012ecf8dad6ce79dc43e42bc1a54de6c") +_TEST_SECRET_KEY_1_BYTES = bytes.fromhex( + "10c78d9bc7ac767ec6252823fff66bc910e2fcd8dac67ed94ce324cba145ed6c") +_TEST_PLAINTEXT = ("T" * int(constants.NOTE_LENGTH / 8)).encode() + + +class TestEncryption(TestCase): + + def test_encrypt_decrypt(self) -> None: + """ + Tests the correct encrypt-decrypt flow: decrypt(encrypt(m)) == m + where m is encoded on NOTE_LENGTH/BYTE_LEN bytes. + """ + sk = encryption.decode_encryption_secret_key(_TEST_SECRET_KEY_BYTES) + pk = encryption.get_encryption_public_key(sk) + ciphertext = encryption.encrypt(_TEST_PLAINTEXT, pk) + plaintext = encryption.decrypt(ciphertext, sk) + self.assertEqual(_TEST_PLAINTEXT, plaintext) + + def test_decryption_invalid_key(self) -> None: + """ + Tests that ONLY the owner of the receiver key can decrypt the ciphertext. + """ + sk_1 = encryption.decode_encryption_secret_key(_TEST_SECRET_KEY_1_BYTES) + sk = encryption.decode_encryption_secret_key(_TEST_SECRET_KEY_BYTES) + pk = encryption.get_encryption_public_key(sk) + ciphertext = encryption.encrypt(_TEST_PLAINTEXT, pk) + with self.assertRaises(encryption.InvalidSignature): + encryption.decrypt(ciphertext, sk_1) + + def test_private_key_generation(self) -> None: + """ + Sample some random keys and ensure that they comply with the + specification. See encryption.py for details. + """ + for _ in range(128): + sk = encryption.generate_encryption_secret_key() + sk_bytes = encryption.encode_encryption_secret_key(sk) + # print(f"key:{sk_bytes.hex()}") + + sk_first_byte = int(sk_bytes[0]) + sk_last_byte = int(sk_bytes[31]) + self.assertEqual( + 0, + sk_first_byte % 8, + "invalid key data (first byte not multiple of 8)") + self.assertTrue( + 64 <= sk_last_byte <= 127, + f"invalid key data (invalid last byte: {sk_last_byte})") diff --git a/test/test_ethervalue.py b/test/test_ethervalue.py new file mode 100644 index 0000000..8b314dc --- /dev/null +++ b/test/test_ethervalue.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from zeth.utils import EtherValue +from unittest import TestCase + + +class TestEthereValue(TestCase): + + def test_conversion(self) -> None: + aval = EtherValue(75641320, 'wei') + aval_eth = aval.ether() + bval = EtherValue(aval_eth, 'ether') + self.assertEqual(aval.wei, bval.wei) + self.assertEqual(aval.ether(), bval.ether()) + + def test_equality(self) -> None: + aval = EtherValue(1.2) + aval_same = EtherValue(1.2) + bval = EtherValue(0.8) + + self.assertEqual(aval, aval) + self.assertEqual(aval, aval_same) + self.assertNotEqual(aval, bval) + + def test_arithmetic(self) -> None: + aval = EtherValue(1.2) + bval = EtherValue(0.8) + cval = EtherValue(0.4) + + self.assertEqual(aval, bval + cval) + self.assertEqual(bval, aval - cval) + + def test_comparison(self) -> None: + big = EtherValue(1.2) + small = EtherValue(0.8) + small_same = EtherValue(0.8) + + self.assertTrue(small < big) + self.assertTrue(small <= big) + self.assertTrue(big > small) + self.assertTrue(big >= small) + self.assertTrue(small_same >= small) + self.assertTrue(small_same <= small) + + self.assertFalse(small > big) + self.assertFalse(small >= big) + self.assertFalse(big < small) + self.assertFalse(big <= small) + self.assertFalse(small_same > small) + self.assertFalse(small_same < small) + + def test_bool(self) -> None: + zero = EtherValue(0) + self.assertFalse(zero) + self.assertTrue(not zero) + + non_zero = EtherValue(0.1) + self.assertTrue(non_zero) + self.assertFalse(not non_zero) diff --git a/test/test_joinsplit.py b/test/test_joinsplit.py new file mode 100644 index 0000000..61fb955 --- /dev/null +++ b/test/test_joinsplit.py @@ -0,0 +1,30 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from zeth.mixer_client import compute_commitment +from api.zeth_messages_pb2 import ZethNote +import zeth.constants as constants + +from unittest import TestCase + + +class TestJoinsplit(TestCase): + + def test_compute_commitment(self) -> None: + """ + Test the commitment value for a note, as computed by the circuit. + """ + apk = "44810c8d62784f5e9ce862925ebb889d1076a453677a5d73567387cd5717a402" + value = "0000000005f5e100" + rho = "0b0bb358233326ce4d346d86f9a0c3778ed8ce15efbf7640aad6e9359145659f" + r = "1e3063320fd43f2d6c456d7f1ee11b7ab486308133e2a5afe916daa4ff5357f6" + cm_expect = int( + "fdf5279335a2fa36fb0d664509808db8d02b6f05f9e5639960952a7038363cfc", + 16) + cm_expect_field = cm_expect % constants.ZETH_PRIME + + note = ZethNote(apk=apk, value=value, rho=rho, trap_r=r) + cm = int.from_bytes(compute_commitment(note), byteorder="big") + + self.assertEqual(cm_expect_field, cm) diff --git a/test/test_merkle_tree.py b/test/test_merkle_tree.py new file mode 100644 index 0000000..ba51c05 --- /dev/null +++ b/test/test_merkle_tree.py @@ -0,0 +1,176 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + + +from zeth.merkle_tree import MerkleTree, PersistentMerkleTree, ZERO_ENTRY, \ + compute_merkle_path +from zeth.utils import extend_32bytes +from os.path import exists, join +from os import makedirs +from shutil import rmtree +from unittest import TestCase +from typing import List + +MERKLE_TREE_TEST_DIR = "_merkle_tests" +MERKLE_TREE_TEST_DEPTH = 4 +MERKLE_TREE_TEST_NUM_LEAVES = pow(2, MERKLE_TREE_TEST_DEPTH) +TEST_VALUES = [ + extend_32bytes(i.to_bytes(1, 'big')) + for i in range(1, MERKLE_TREE_TEST_NUM_LEAVES)] + + +class TestMerkleTree(TestCase): + + @staticmethod + def setUpClass() -> None: + TestMerkleTree.tearDownClass() + makedirs(MERKLE_TREE_TEST_DIR) + + @staticmethod + def tearDownClass() -> None: + if exists(MERKLE_TREE_TEST_DIR): + rmtree(MERKLE_TREE_TEST_DIR) + + def test_combine(self) -> None: + # Use test vectors used to test the MiMC contract (generated in + # test_mimc.py) + + left = self._test_vector_to_bytes32( + 3703141493535563179657531719960160174296085208671919316200479060314459804651) # noqa + right = self._test_vector_to_bytes32( + 15683951496311901749339509118960676303290224812129752890706581988986633412003) # noqa + expect = self._test_vector_to_bytes32( + 16797922449555994684063104214233396200599693715764605878168345782964540311877) # noqa + + result = MerkleTree.combine(left, right) + self.assertEqual(expect, result) + + def test_empty(self) -> None: + mktree = MerkleTree.empty_with_size(MERKLE_TREE_TEST_NUM_LEAVES) + root = mktree.recompute_root() + num_entries = mktree.get_num_entries() + + self.assertEqual(0, num_entries) + self.assertEqual(self._expected_empty(), root) + + def test_empty_save_load(self) -> None: + mktree_file = join(MERKLE_TREE_TEST_DIR, "empty_save_load") + mktree = PersistentMerkleTree.open( + mktree_file, MERKLE_TREE_TEST_NUM_LEAVES) + mktree.save() + + mktree = PersistentMerkleTree.open( + mktree_file, MERKLE_TREE_TEST_NUM_LEAVES) + root = mktree.recompute_root() + mktree.save() + + self.assertEqual(self._expected_empty(), root) + + def test_single_entry(self) -> None: + mktree_file = join(MERKLE_TREE_TEST_DIR, "single") + data = TEST_VALUES[0] + + mktree = PersistentMerkleTree.open( + mktree_file, MERKLE_TREE_TEST_NUM_LEAVES) + mktree.insert(data) + self.assertEqual(1, mktree.get_num_entries()) + self.assertEqual(data, mktree.get_leaf(0)) + self.assertEqual(ZERO_ENTRY, mktree.get_leaf(1)) + root_1 = mktree.recompute_root() + self.assertEqual( + MerkleTree.combine(data, ZERO_ENTRY), mktree.get_node(1, 0)) + self.assertNotEqual(self._expected_empty(), root_1) + mktree.save() + + mktree = PersistentMerkleTree.open( + mktree_file, MERKLE_TREE_TEST_NUM_LEAVES) + self.assertEqual(1, mktree.get_num_entries()) + self.assertEqual(data, mktree.get_leaf(0)) + self.assertEqual(ZERO_ENTRY, mktree.get_leaf(1)) + root_2 = mktree.recompute_root() + self.assertEqual(root_1, root_2) + + def test_single_entry_all_nodes(self) -> None: + mktree = MerkleTree.empty_with_size(MERKLE_TREE_TEST_NUM_LEAVES) + mktree.insert(TEST_VALUES[0]) + _ = mktree.recompute_root() + self._check_tree_nodes([TEST_VALUES[0]], mktree) + + self.assertEqual( + mktree.recompute_root(), + mktree.get_node(MERKLE_TREE_TEST_DEPTH, 0)) + + def test_multiple_entries_all_nodes(self) -> None: + mktree = MerkleTree.empty_with_size(MERKLE_TREE_TEST_NUM_LEAVES) + mktree.insert(TEST_VALUES[0]) + mktree.insert(TEST_VALUES[1]) + mktree.insert(TEST_VALUES[2]) + _ = mktree.recompute_root() + self._check_tree_nodes( + [TEST_VALUES[0], TEST_VALUES[1], TEST_VALUES[2]], mktree) + + def test_merkle_path(self) -> None: + tree_size = MERKLE_TREE_TEST_NUM_LEAVES + + def _check_path_for_num_entries(num_entries: int, address: int) -> None: + mktree = MerkleTree.empty_with_size(tree_size) + for val in TEST_VALUES[0:num_entries]: + mktree.insert(val) + _ = mktree.recompute_root() + mkpath = compute_merkle_path(address, mktree) + self._check_merkle_path(address, mkpath, mktree) + + _check_path_for_num_entries(3, 0) + _check_path_for_num_entries(3, 1) + _check_path_for_num_entries(3, 2) + _check_path_for_num_entries(4, 0) + _check_path_for_num_entries(4, 1) + _check_path_for_num_entries(4, 2) + _check_path_for_num_entries(4, 3) + _check_path_for_num_entries(5, 0) + _check_path_for_num_entries(5, 1) + _check_path_for_num_entries(5, 2) + _check_path_for_num_entries(5, 3) + _check_path_for_num_entries(5, 4) + + @staticmethod + def _test_vector_to_bytes32(value: int) -> bytes: + return value.to_bytes(32, byteorder='big') + + def _expected_empty(self) -> bytes: + self.assertEqual(16, MERKLE_TREE_TEST_NUM_LEAVES) + # Test vector generated by test_mimc.py + return self._test_vector_to_bytes32( + 1792447880902456454889084480864374954299744757125100424674028184042059183092) # noqa + + def _check_merkle_path( + self, address: int, mkpath: List[str], mktree: MerkleTree) -> None: + self.assertEqual(len(mkpath), mktree.depth) + current = mktree.get_node(0, address) + for i in range(mktree.depth): + if address & 1: + current = MerkleTree.combine(bytes.fromhex(mkpath[i]), current) + else: + current = MerkleTree.combine(current, bytes.fromhex(mkpath[i])) + address = address >> 1 + + self.assertEqual(mktree.get_root(), current) + + def _check_tree_nodes(self, leaves: List[bytes], mktree: MerkleTree) -> None: + def layer_size(layer: int) -> int: + return int(MERKLE_TREE_TEST_NUM_LEAVES / pow(2, layer)) + + # Check layer 0 + _, layer_0 = next(mktree.get_layers()) + self.assertEqual(leaves, layer_0) + + # Check layer `layer` + for layer in range(1, MERKLE_TREE_TEST_DEPTH): + for i in range(layer_size(layer)): + self.assertEqual( + MerkleTree.combine( + mktree.get_node(layer - 1, 2 * i), + mktree.get_node(layer - 1, 2 * i + 1)), + mktree.get_node(layer, i), + f"Layer {layer}, node {i}") diff --git a/test/test_mimc.py b/test/test_mimc.py new file mode 100644 index 0000000..bddb2ed --- /dev/null +++ b/test/test_mimc.py @@ -0,0 +1,18 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from zeth.mimc import MiMC7 +from unittest import TestCase + + +class TestMiMC(TestCase): + + @staticmethod + def test_mimc_round() -> None: + m = MiMC7("Clearmatics") + x = 340282366920938463463374607431768211456 + k = 28948022309329048855892746252171976963317496166410141009864396001978282409983 # noqa + c = 14220067918847996031108144435763672811050758065945364308986253046354060608451 # noqa + assert m.mimc_round(x, k, c) == \ + 7970444205539657036866618419973693567765196138501849736587140180515018751924 # noqa diff --git a/test/test_signing.py b/test/test_signing.py new file mode 100644 index 0000000..fc5364a --- /dev/null +++ b/test/test_signing.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from zeth import signing +from hashlib import sha256 +from unittest import TestCase +from os import urandom + + +class TestSigning(TestCase): + + keypair = signing.gen_signing_keypair() + + def test_sign_verify(self) -> None: + """ + Test the correct signing-verification flow: + verify(vk, sign(sk,m), m) = 1 + """ + m = sha256("clearmatics".encode()).digest() + sigma = signing.sign(self.keypair.sk, m) + self.assertTrue(signing.verify(self.keypair.vk, m, sigma)) + + keypair2 = signing.gen_signing_keypair() + self.assertFalse(signing.verify(keypair2.vk, m, sigma)) + + def test_sign_verify_random(self) -> None: + """ + Test the correct signing-verification flow with random message: + verify(vk, sign(sk,m), m) = 1 + """ + m = urandom(32) + sigma = signing.sign(self.keypair.sk, m) + self.assertTrue(signing.verify(self.keypair.vk, m, sigma)) + + keypair2 = signing.gen_signing_keypair() + self.assertFalse(signing.verify(keypair2.vk, m, sigma)) + + def test_signature_encoding(self) -> None: + """ + Test encoding and decoding of signatures. + """ + m = sha256("clearmatics".encode()).digest() + sig = signing.sign(self.keypair.sk, m) + sig_encoded = signing.encode_signature_to_bytes(sig) + sig_decoded = signing.decode_signature_from_bytes(sig_encoded) + self.assertEqual(sig, sig_decoded) diff --git a/test_commands/__init__.py b/test_commands/__init__.py new file mode 100644 index 0000000..6fb9229 --- /dev/null +++ b/test_commands/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ diff --git a/test_commands/deploy_test_token.py b/test_commands/deploy_test_token.py new file mode 100644 index 0000000..141c53b --- /dev/null +++ b/test_commands/deploy_test_token.py @@ -0,0 +1,88 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from zeth.contracts import Interface +from zeth.utils import get_zeth_dir +from zeth.constants import SOL_COMPILER_VERSION +from test_commands.mock import open_test_web3 +from click import command, argument +from os.path import join +from solcx import compile_files, set_solc_version +from typing import Any +from web3 import Web3 # type: ignore + + +@command() +@argument("deployer_address") +@argument("mint_amount", type=int) +@argument("recipient_address") +def deploy_test_token( + deployer_address: str, + mint_amount: int, + recipient_address: str) -> None: + """ + Deploy a simple ERC20 token for testing, and mint some for a specific + address. Print the token address. + """ + _, eth = open_test_web3() + token_instance = deploy_token(eth, deployer_address, 4000000) + mint_tx_hash = mint_token( + token_instance, recipient_address, deployer_address, mint_amount) + eth.waitForTransactionReceipt(mint_tx_hash) + print(token_instance.address) + + +def compile_token() -> Interface: + """ + Compile the testing ERC20 token contract + """ + + zeth_dir = get_zeth_dir() + allowed_path = join( + zeth_dir, + "zeth_contracts/node_modules/openzeppelin-solidity/contracts") + path_to_token = join( + zeth_dir, + "zeth_contracts/node_modules/openzeppelin-solidity/contracts", + "token/ERC20/ERC20Mintable.sol") + # Compilation + set_solc_version(SOL_COMPILER_VERSION) + compiled_sol = compile_files([path_to_token], allow_paths=allowed_path) + token_interface = compiled_sol[path_to_token + ":ERC20Mintable"] + return token_interface + + +def deploy_token( + eth: Any, + deployer_address: str, + deployment_gas: int) -> Any: + """ + Deploy the testing ERC20 token contract + """ + token_interface = compile_token() + token = eth.contract( + abi=token_interface['abi'], bytecode=token_interface['bin']) + tx_hash = token.constructor().transact( + {'from': deployer_address, 'gas': deployment_gas}) + tx_receipt = eth.waitForTransactionReceipt(tx_hash) + + token = eth.contract( + address=tx_receipt.contractAddress, + abi=token_interface['abi'], + ) + return token + + +def mint_token( + token_instance: Any, + spender_address: str, + deployer_address: str, + token_amount: int) -> bytes: + return token_instance.functions.mint( + spender_address, + Web3.toWei(token_amount, 'ether')).transact({'from': deployer_address}) + + +if __name__ == "__main__": + deploy_test_token() # pylint: disable=no-value-for-parameter diff --git a/test_commands/get_balance.py b/test_commands/get_balance.py new file mode 100644 index 0000000..c17fd69 --- /dev/null +++ b/test_commands/get_balance.py @@ -0,0 +1,30 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from test_commands.mock import open_test_web3 +from zeth.utils import EtherValue +from click import command, argument, option +from typing import List + + +@command() +@argument("addresses", nargs=-1) +@option( + "--wei", + is_flag=True, + default=False, + help="Display in Wei instead of Ether") +def get_balance(addresses: List[str], wei: bool) -> None: + """ + Command to get the balance of specific addresses. Support multiple queries + per invocation (outputs one per line), for efficiency. + """ + _, eth = open_test_web3() + for address in addresses: + value = EtherValue(eth.getBalance(address), "wei") + print((wei and value.wei) or value.ether()) + + +if __name__ == "__main__": + get_balance() # pylint: disable=no-value-for-parameter diff --git a/test_commands/mock.py b/test_commands/mock.py new file mode 100644 index 0000000..08c2c3b --- /dev/null +++ b/test_commands/mock.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from zeth.zeth_address import ZethAddress +from zeth.encryption import EncryptionKeyPair, decode_encryption_secret_key, \ + decode_encryption_public_key +from zeth.ownership import gen_ownership_keypair +from zeth.utils import get_contracts_dir, open_web3 +from os.path import join +from solcx import compile_files # type: ignore +from typing import Dict, List, Tuple, Optional, Any + +# Web3 HTTP provider +TEST_PROVER_SERVER_ENDPOINT: str = "localhost:50051" +TEST_WEB3_PROVIDER_ENDPOINT: str = "http://localhost:8545" +TEST_NOTE_DIR: str = "_test_notes" + +KeyStore = Dict[str, ZethAddress] + + +def open_test_web3() -> Tuple[Any, Any]: + web3 = open_web3(TEST_WEB3_PROVIDER_ENDPOINT) + return web3, web3.eth # pylint: disable=no-member # type: ignore + + +def init_test_keystore() -> KeyStore: + """ + Keystore for the tests + """ + + alice_25519_enc_private_key = \ + b'\xde\xa2\xc1\x0b\xd1\xf7\x13\xf8J\xa4:\xa4\xb6\xfa\xbd\xd5\xc9' + \ + b'\x8a\xd9\xb6\xb4\xc4\xc4I\x88\xa4\xd9\xe2\xee\x9e\x9a\xff' + alice_25519_enc_public_key = \ + b'\x1eO"\n\xdaWnU+\xf5\xaa\x8a#\xd2*\xd3\x11\x9fc\xe52 \xd8^\xbc-' + \ + b'\xb6\xf1\xeej\xf41' + + bob_25519_enc_private_key = \ + b'\xd3\xf0\x8f ,\x1d#\xdc\xac,\x93\xbd\xd0\xd9\xed\x8c\x92\x822' + \ + b'\xef\xd6\x97^\x86\xf7\xe4/\x85\xb6\x10\xe6o' + bob_25519_enc_public_key = \ + b't\xc5{5j\xb5\x8a\xd3n\xb3\xab9\xe8s^13\xba\xa2\x91x\xb01(\xf9' + \ + b'\xbb\xf9@r_\x91}' + + charlie_25519_enc_private_key = b'zH\xb66q\x97\x0bO\xcb\xb9q\x9b\xbd-1`I' + \ + b'\xae\x00-\x11\xb9\xed}\x18\x9f\xf6\x8dr\xaa\xd4R' + charlie_25519_enc_public_key = \ + b'u\xe7\x88\x9c\xbfE(\xf8\x99\xca<\xa8[<\xa2\x88m\xad\rN"\xf0}' + \ + b'\xec\xfcB\x89\xe6\x96\xcf\x19U' + + # Alice credentials in the zeth abstraction + alice_ownership = gen_ownership_keypair() + alice_encryption = EncryptionKeyPair( + decode_encryption_secret_key(alice_25519_enc_private_key), + decode_encryption_public_key(alice_25519_enc_public_key)) + + # Bob credentials in the zeth abstraction + bob_ownership = gen_ownership_keypair() + bob_encryption = EncryptionKeyPair( + decode_encryption_secret_key(bob_25519_enc_private_key), + decode_encryption_public_key(bob_25519_enc_public_key)) + + # Charlie credentials in the zeth abstraction + charlie_ownership = gen_ownership_keypair() + charlie_encryption = EncryptionKeyPair( + decode_encryption_secret_key(charlie_25519_enc_private_key), + decode_encryption_public_key(charlie_25519_enc_public_key)) + + return { + "Alice": ZethAddress.from_key_pairs( + alice_ownership, alice_encryption), + "Bob": ZethAddress.from_key_pairs( + bob_ownership, bob_encryption), + "Charlie": ZethAddress.from_key_pairs( + charlie_ownership, charlie_encryption), + } + + +def get_dummy_merkle_path(length: int) -> List[str]: + mk_path = [] + # Arbitrary sha256 digest used to build the dummy merkle path + dummy_node = \ + "6461f753bfe21ba2219ced74875b8dbd8c114c3c79d7e41306dd82118de1895b" + for _ in range(length): + mk_path.append(dummy_node) + return mk_path + + +def deploy_contract( + eth: Any, + deployer_address: str, + contract_name: str, + constructor_args: Optional[Dict[str, Any]] = None) -> Tuple[Any, Any]: + contracts_dir = get_contracts_dir() + sol_path = join(contracts_dir, contract_name + ".sol") + compiled_sol = compile_files([sol_path]) + interface = compiled_sol[sol_path + ":" + contract_name] + contract_abi = interface['abi'] + contract = eth.contract(abi=contract_abi, bytecode=interface['bin']) + deploy_tx = contract.constructor(**constructor_args) + deploy_tx_hash = deploy_tx.transact({'from': deployer_address}) + tx_receipt = eth.waitForTransactionReceipt(deploy_tx_hash, 1000) + contract_address = tx_receipt['contractAddress'] + contract_instance = eth.contract( + address=contract_address, + abi=contract_abi) + return interface, contract_instance diff --git a/test_commands/scenario.py b/test_commands/scenario.py new file mode 100644 index 0000000..e0ab942 --- /dev/null +++ b/test_commands/scenario.py @@ -0,0 +1,461 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from zeth.mixer_client import MixerClient, OwnershipKeyPair, joinsplit_sign, \ + encrypt_notes, get_dummy_input_and_address, compute_h_sig, \ + JoinsplitSigVerificationKey +import zeth.contracts as contracts +from zeth.constants import ZETH_PRIME, FIELD_CAPACITY, DEFAULT_MIX_GAS_WEI +import zeth.signing as signing +from zeth.merkle_tree import MerkleTree, compute_merkle_path +from zeth.utils import EtherValue, to_zeth_units +import test_commands.mock as mock +from api.zeth_messages_pb2 import ZethNote + +from os import urandom +from web3 import Web3 # type: ignore +from typing import List, Tuple, Optional + +ZERO_UNITS_HEX = "0000000000000000" +BOB_DEPOSIT_ETH = 200 +BOB_SPLIT_1_ETH = 100 +BOB_SPLIT_2_ETH = 100 + +BOB_TO_CHARLIE_ETH = 50 +BOB_TO_CHARLIE_CHANGE_ETH = BOB_SPLIT_1_ETH - BOB_TO_CHARLIE_ETH + +CHARLIE_WITHDRAW_ETH = 10.5 +CHARLIE_WITHDRAW_CHANGE_ETH = 39.5 + + +def dump_merkle_tree(mk_tree: List[bytes]) -> None: + print("[DEBUG] Displaying the Merkle tree of commitments: ") + for node in mk_tree: + print("Node: " + Web3.toHex(node)[2:]) + + +def wait_for_tx_update_mk_tree( + zeth_client: MixerClient, + mk_tree: MerkleTree, + tx_hash: str) -> contracts.MixResult: + tx_receipt = zeth_client.web3.eth.waitForTransactionReceipt(tx_hash, 10000) + result = contracts.parse_mix_call(zeth_client.mixer_instance, tx_receipt) + for out_ev in result.output_events: + mk_tree.insert(out_ev.commitment) + + if mk_tree.recompute_root() != result.new_merkle_root: + raise Exception("Merkle root mismatch between log and local tree") + return result + + +def bob_deposit( + zeth_client: MixerClient, + mk_tree: MerkleTree, + bob_eth_address: str, + keystore: mock.KeyStore, + tx_value: Optional[EtherValue] = None) -> contracts.MixResult: + print( + f"=== Bob deposits {BOB_DEPOSIT_ETH} ETH for himself and splits into " + + f"note1: {BOB_SPLIT_1_ETH}ETH, note2: {BOB_SPLIT_2_ETH}ETH ===") + + bob_js_keypair = keystore["Bob"] + bob_addr = keystore["Bob"].addr_pk + + outputs = [ + (bob_addr, EtherValue(BOB_SPLIT_1_ETH)), + (bob_addr, EtherValue(BOB_SPLIT_2_ETH)), + ] + + tx_hash = zeth_client.deposit( + mk_tree, + bob_js_keypair, + bob_eth_address, + EtherValue(BOB_DEPOSIT_ETH), + outputs, + tx_value) + return wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) + + +def bob_to_charlie( + zeth_client: MixerClient, + mk_tree: MerkleTree, + input1: Tuple[int, ZethNote], + bob_eth_address: str, + keystore: mock.KeyStore) -> contracts.MixResult: + print( + f"=== Bob transfers {BOB_TO_CHARLIE_ETH}ETH to Charlie from his funds " + + "on the mixer ===") + + bob_ask = keystore["Bob"].addr_sk.a_sk + charlie_addr = keystore["Charlie"].addr_pk + bob_addr = keystore["Bob"].addr_pk + + # Coin for Bob (change) + output0 = (bob_addr, EtherValue(BOB_TO_CHARLIE_ETH)) + # Coin for Charlie + output1 = (charlie_addr, EtherValue(BOB_TO_CHARLIE_CHANGE_ETH)) + + # Send the tx + tx_hash = zeth_client.joinsplit( + mk_tree, + OwnershipKeyPair(bob_ask, bob_addr.a_pk), + bob_eth_address, + [input1], + [output0, output1], + EtherValue(0), + EtherValue(0), + EtherValue(1, 'wei')) + return wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) + + +def charlie_withdraw( + zeth_client: MixerClient, + mk_tree: MerkleTree, + input1: Tuple[int, ZethNote], + charlie_eth_address: str, + keystore: mock.KeyStore) -> contracts.MixResult: + print( + f" === Charlie withdraws {CHARLIE_WITHDRAW_ETH}ETH from his funds " + + "on the Mixer ===") + + charlie_pk = keystore["Charlie"].addr_pk + charlie_apk = charlie_pk.a_pk + charlie_ask = keystore["Charlie"].addr_sk.a_sk + charlie_ownership_key = \ + OwnershipKeyPair(charlie_ask, charlie_apk) + + tx_hash = zeth_client.joinsplit( + mk_tree, + charlie_ownership_key, + charlie_eth_address, + [input1], + [(charlie_pk, EtherValue(CHARLIE_WITHDRAW_CHANGE_ETH))], + EtherValue(0), + EtherValue(CHARLIE_WITHDRAW_ETH), + EtherValue(1, 'wei')) + return wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) + + +def charlie_double_withdraw( + zeth_client: MixerClient, + mk_tree: MerkleTree, + input1: Tuple[int, ZethNote], + charlie_eth_address: str, + keystore: mock.KeyStore) -> contracts.MixResult: + """ + Charlie tries to carry out a double spending by modifying the value of the + nullifier of the previous payment + """ + print( + f" === Charlie attempts to withdraw {CHARLIE_WITHDRAW_ETH}ETH once " + + "more (double spend) one of his note on the Mixer ===") + + charlie_apk = keystore["Charlie"].addr_pk.a_pk + charlie_ask = keystore["Charlie"].addr_sk.a_sk + + tree_depth = mk_tree.depth + mk_path1 = compute_merkle_path(input1[0], mk_tree) + mk_root = mk_tree.get_root() + + # Create the an additional dummy input for the MixerClient + input2 = get_dummy_input_and_address(charlie_apk) + dummy_mk_path = mock.get_dummy_merkle_path(tree_depth) + + note1_value = to_zeth_units(EtherValue(CHARLIE_WITHDRAW_CHANGE_ETH)) + v_out = EtherValue(CHARLIE_WITHDRAW_ETH) + + # ### ATTACK BLOCK + # Add malicious nullifiers: we reuse old nullifiers to double spend by + # adding $r$ to them so that they have the same value as before in Z_r, + # and so the zksnark verification passes, but have different values in + # {0;1}^256 so that they appear different to the contract. + # See: https://github.com/clearmatics/zeth/issues/38 + + attack_primary_input3: int = 0 + attack_primary_input4: int = 0 + + def compute_h_sig_attack_nf( + nf0: bytes, + nf1: bytes, + sign_vk: JoinsplitSigVerificationKey) -> bytes: + # We disassemble the nfs to get the formatting of the primary inputs + input_nullifier0 = nf0.hex() + input_nullifier1 = nf1.hex() + nf0_rev = "{0:0256b}".format(int(input_nullifier0, 16)) + primary_input3_bits = nf0_rev[:FIELD_CAPACITY] + primary_input3_res_bits = nf0_rev[FIELD_CAPACITY:] + nf1_rev = "{0:0256b}".format(int(input_nullifier1, 16)) + primary_input4_bits = nf1_rev[:FIELD_CAPACITY] + primary_input4_res_bits = nf1_rev[FIELD_CAPACITY:] + + # We perform the attack, recoding the modified public input values + nonlocal attack_primary_input3 + nonlocal attack_primary_input4 + attack_primary_input3 = int(primary_input3_bits, 2) + ZETH_PRIME + attack_primary_input4 = int(primary_input4_bits, 2) + ZETH_PRIME + + # We reassemble the nfs + attack_primary_input3_bits = "{0:0256b}".format(attack_primary_input3) + attack_nf0_bits = attack_primary_input3_bits[ + len(attack_primary_input3_bits) - FIELD_CAPACITY:] +\ + primary_input3_res_bits + attack_nf0 = "{0:064x}".format(int(attack_nf0_bits, 2)) + attack_primary_input4_bits = "{0:0256b}".format(attack_primary_input4) + attack_nf1_bits = attack_primary_input4_bits[ + len(attack_primary_input4_bits) - FIELD_CAPACITY:] +\ + primary_input4_res_bits + attack_nf1 = "{0:064x}".format(int(attack_nf1_bits, 2)) + return compute_h_sig( + bytes.fromhex(attack_nf0), bytes.fromhex(attack_nf1), sign_vk) + + (output_note1, output_note2, proof_json, signing_keypair) = \ + zeth_client.get_proof_joinsplit_2_by_2( + mk_root, + input1, + mk_path1, + input2, + dummy_mk_path, + charlie_ask, # sender + (charlie_apk, note1_value), # recipient1 + (charlie_apk, 0), # recipient2 + to_zeth_units(EtherValue(0)), # v_in + to_zeth_units(v_out), # v_out + compute_h_sig_attack_nf) + + # Update the primary inputs to the modified nullifiers, since libsnark + # overwrites them with values in Z_p + + assert attack_primary_input3 != 0 + assert attack_primary_input4 != 0 + + print("proof_json => ", proof_json) + print("proof_json[inputs][3] => ", proof_json["inputs"][3]) + print("proof_json[inputs][4] => ", proof_json["inputs"][4]) + proof_json["inputs"][3] = hex(attack_primary_input3) + proof_json["inputs"][4] = hex(attack_primary_input4) + # ### ATTACK BLOCK + + # construct pk object from bytes + pk_charlie = keystore["Charlie"].addr_pk.k_pk + + # encrypt the coins + ciphertexts = encrypt_notes([ + (output_note1, pk_charlie), + (output_note2, pk_charlie)]) + + # Compute the joinSplit signature + joinsplit_sig_charlie = joinsplit_sign( + signing_keypair, + charlie_eth_address, + ciphertexts, + proof_json) + + mix_params = contracts.MixParameters( + proof_json, + signing_keypair.vk, + joinsplit_sig_charlie, + ciphertexts) + + tx_hash = zeth_client.mix( + mix_params, + charlie_eth_address, + # Pay an arbitrary amount (1 wei here) that will be refunded since the + # `mix` function is payable + Web3.toWei(1, 'wei'), + DEFAULT_MIX_GAS_WEI) + return wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) + + +def charlie_corrupt_bob_deposit( + zeth_client: MixerClient, + mk_tree: MerkleTree, + bob_eth_address: str, + charlie_eth_address: str, + keystore: mock.KeyStore) -> contracts.MixResult: + """ + Charlie tries to break transaction malleability and corrupt the coins + bob is sending in a transaction + She does so by intercepting bob's transaction and either: + - case 1: replacing the ciphertexts (or sender_eph_pk) by garbage/arbitrary + data + - case 2: replacing the ciphertexts by garbage/arbitrary data and using a + new OT-signature + - case 3: Charlie replays the mix call of Bob, to try to receive the vout + Both attacks should fail, + - case 1: the signature check should fail, else Charlie broke UF-CMA of the + OT signature + - case 2: the h_sig/vk verification should fail, as h_sig is not a function + of vk any longer + - case 3: the signature check should fail, because `msg.sender` will no match + the value used in the mix parameters (Bob's Ethereum Address). + NB. If the adversary were to corrupt the ciphertexts (or the encryption key), + replace the OT-signature by a new one and modify the h_sig accordingly so that + the check on the signature verification (key h_sig/vk) passes, the proof would + not verify, which is why we do not test this case. + """ + print( + f"=== Bob deposits {BOB_DEPOSIT_ETH} ETH for himself and split into " + + f"note1: {BOB_SPLIT_1_ETH}ETH, note2: {BOB_SPLIT_2_ETH}ETH" + + f"but Charlie attempts to corrupt the transaction ===") + bob_apk = keystore["Bob"].addr_pk.a_pk + bob_ask = keystore["Bob"].addr_sk.a_sk + tree_depth = mk_tree.depth + mk_root = mk_tree.get_root() + # mk_tree_depth = zeth_client.mk_tree_depth + # mk_root = zeth_client.merkle_root + + # Create the JoinSplit dummy inputs for the deposit + input1 = get_dummy_input_and_address(bob_apk) + input2 = get_dummy_input_and_address(bob_apk) + dummy_mk_path = mock.get_dummy_merkle_path(tree_depth) + + note1_value = to_zeth_units(EtherValue(BOB_SPLIT_1_ETH)) + note2_value = to_zeth_units(EtherValue(BOB_SPLIT_2_ETH)) + + v_in = to_zeth_units(EtherValue(BOB_DEPOSIT_ETH)) + + (output_note1, output_note2, proof_json, joinsplit_keypair) = \ + zeth_client.get_proof_joinsplit_2_by_2( + mk_root, + input1, + dummy_mk_path, + input2, + dummy_mk_path, + bob_ask, # sender + (bob_apk, note1_value), # recipient1 + (bob_apk, note2_value), # recipient2 + v_in, # v_in + to_zeth_units(EtherValue(0)) # v_out + ) + + # Encrypt the coins to bob + pk_bob = keystore["Bob"].addr_pk.k_pk + ciphertexts = encrypt_notes([ + (output_note1, pk_bob), + (output_note2, pk_bob)]) + + # ### ATTACK BLOCK + # Charlie intercepts Bob's deposit, corrupts it and + # sends her transaction before Bob's transaction is accepted + + # Case 1: replacing the ciphertexts by garbage/arbitrary data + # Corrupt the ciphertexts + # (another way would have been to overwrite sender_eph_pk) + fake_ciphertext0 = urandom(32) + fake_ciphertext1 = urandom(32) + + result_corrupt1 = None + try: + joinsplit_sig_charlie = joinsplit_sign( + joinsplit_keypair, + charlie_eth_address, + ciphertexts, + proof_json) + + mix_params = contracts.MixParameters( + proof_json, + joinsplit_keypair.vk, + joinsplit_sig_charlie, + [fake_ciphertext0, fake_ciphertext1]) + tx_hash = zeth_client.mix( + mix_params, + charlie_eth_address, + Web3.toWei(BOB_DEPOSIT_ETH, 'ether'), + DEFAULT_MIX_GAS_WEI) + result_corrupt1 = \ + wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) + except Exception as e: + print( + f"Charlie's first corruption attempt" + + f" successfully rejected! (msg: {e})" + ) + assert(result_corrupt1 is None), \ + "Charlie managed to corrupt Bob's deposit the first time!" + print("") + + # Case 2: replacing the ciphertexts by garbage/arbitrary data and + # using a new OT-signature + # Corrupt the ciphertexts + fake_ciphertext0 = urandom(32) + fake_ciphertext1 = urandom(32) + new_joinsplit_keypair = signing.gen_signing_keypair() + + # Sign the primary inputs, sender_eph_pk and the ciphertexts + + result_corrupt2 = None + try: + joinsplit_sig_charlie = joinsplit_sign( + new_joinsplit_keypair, + charlie_eth_address, + [fake_ciphertext0, fake_ciphertext1], + proof_json) + mix_params = contracts.MixParameters( + proof_json, + new_joinsplit_keypair.vk, + joinsplit_sig_charlie, + [fake_ciphertext0, fake_ciphertext1]) + tx_hash = zeth_client.mix( + mix_params, + charlie_eth_address, + Web3.toWei(BOB_DEPOSIT_ETH, 'ether'), + DEFAULT_MIX_GAS_WEI) + result_corrupt2 = \ + wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) + except Exception as e: + print( + f"Charlie's second corruption attempt" + + f" successfully rejected! (msg: {e})" + ) + assert(result_corrupt2 is None), \ + "Charlie managed to corrupt Bob's deposit the second time!" + + # Case3: Charlie uses the correct mix data, but attempts to send the mix + # call from his own address (thereby receiving the output). + result_corrupt3 = None + try: + joinsplit_sig_bob = joinsplit_sign( + joinsplit_keypair, + bob_eth_address, + ciphertexts, + proof_json) + mix_params = contracts.MixParameters( + proof_json, + joinsplit_keypair.vk, + joinsplit_sig_bob, + ciphertexts) + tx_hash = zeth_client.mix( + mix_params, + charlie_eth_address, + Web3.toWei(BOB_DEPOSIT_ETH, 'ether'), + 4000000) + result_corrupt3 = \ + wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) + except Exception as e: + print( + f"Charlie's third corruption attempt" + + f" successfully rejected! (msg: {e})" + ) + assert(result_corrupt3 is None), \ + "Charlie managed to corrupt Bob's deposit the third time!" + # ### ATTACK BLOCK + + # Bob transaction is finally mined + joinsplit_sig_bob = joinsplit_sign( + joinsplit_keypair, + bob_eth_address, + ciphertexts, + proof_json) + mix_params = contracts.MixParameters( + proof_json, + joinsplit_keypair.vk, + joinsplit_sig_bob, + ciphertexts) + tx_hash = zeth_client.mix( + mix_params, + bob_eth_address, + Web3.toWei(BOB_DEPOSIT_ETH, 'ether'), + DEFAULT_MIX_GAS_WEI) + return wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) diff --git a/test_commands/test_erc_token_mixing.py b/test_commands/test_erc_token_mixing.py new file mode 100644 index 0000000..2c8d739 --- /dev/null +++ b/test_commands/test_erc_token_mixing.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +import zeth.merkle_tree +import zeth.utils +import zeth.constants as constants +from zeth.zeth_address import ZethAddressPriv +from zeth.contracts import MixOutputEvents +from zeth.mixer_client import MixerClient +from zeth.wallet import Wallet, ZethNoteDescription +import test_commands.mock as mock +import test_commands.scenario as scenario +from test_commands.deploy_test_token import deploy_token, mint_token +from os.path import join, exists +import shutil +from web3 import Web3 # type: ignore +from typing import Dict, List, Any + + +def print_token_balances( + token_instance: Any, + bob: str, + alice: str, + charlie: str, + mixer: str) -> None: + print("BALANCES:") + print(f" Alice : {token_instance.functions.balanceOf(alice).call()}") + print(f" Bob : {token_instance.functions.balanceOf(bob).call()}") + print(f" Charlie : {token_instance.functions.balanceOf(charlie).call()}") + print(f" Mixer : {token_instance.functions.balanceOf(mixer).call()}") + + +def approve( + token_instance: Any, + owner_address: str, + spender_address: str, + token_amount: int) -> str: + return token_instance.functions.approve( + spender_address, + Web3.toWei(token_amount, 'ether')).transact({'from': owner_address}) + + +def allowance( + token_instance: Any, + owner_address: str, + spender_address: str) -> str: + return token_instance.functions.allowance(owner_address, spender_address) \ + .call() + + +def main() -> None: + + zksnark = zeth.zksnark.get_zksnark_provider(zeth.utils.parse_zksnark_arg()) + web3, eth = mock.open_test_web3() + + # Ethereum addresses + deployer_eth_address = eth.accounts[0] + bob_eth_address = eth.accounts[1] + alice_eth_address = eth.accounts[2] + charlie_eth_address = eth.accounts[3] + # Zeth addresses + keystore = mock.init_test_keystore() + + # Deploy the token contract + token_instance = deploy_token(eth, deployer_eth_address, 4000000) + + # Deploy Zeth contracts + tree_depth = constants.ZETH_MERKLE_TREE_DEPTH + zeth_client, _contract_desc = MixerClient.deploy( + web3, + mock.TEST_PROVER_SERVER_ENDPOINT, + deployer_eth_address, + token_instance.address, + None, + zksnark) + mk_tree = zeth.merkle_tree.MerkleTree.empty_with_depth(tree_depth) + mixer_instance = zeth_client.mixer_instance + + # Keys and wallets + def _mk_wallet(name: str, sk: ZethAddressPriv) -> Wallet: + wallet_dir = join(mock.TEST_NOTE_DIR, name + "-erc") + if exists(wallet_dir): + # Note: symlink-attack resistance + # https://docs.python.org/3/library/shutil.html#shutil.rmtree.avoids_symlink_attacks + shutil.rmtree(wallet_dir) + return Wallet(mixer_instance, name, wallet_dir, sk) + sk_alice = keystore["Alice"].addr_sk + sk_bob = keystore["Bob"].addr_sk + sk_charlie = keystore["Charlie"].addr_sk + alice_wallet = _mk_wallet('alice', sk_alice) + bob_wallet = _mk_wallet('bob', sk_bob) + charlie_wallet = _mk_wallet('charlie', sk_charlie) + block_num = 1 + + # Universal update function + def _receive_notes( + out_ev: List[MixOutputEvents]) \ + -> Dict[str, List[ZethNoteDescription]]: + nonlocal block_num + notes = { + 'alice': alice_wallet.receive_notes(out_ev), + 'bob': bob_wallet.receive_notes(out_ev), + 'charlie': charlie_wallet.receive_notes(out_ev), + } + alice_wallet.update_and_save_state(block_num) + bob_wallet.update_and_save_state(block_num) + charlie_wallet.update_and_save_state(block_num) + block_num = block_num + 1 + return notes + + print("[INFO] 4. Running tests (asset mixed: ERC20 token)...") + # We assign ETHToken to Bob + mint_token( + token_instance, + bob_eth_address, + deployer_eth_address, + 2*scenario.BOB_DEPOSIT_ETH) + print("- Initial balances: ") + print_token_balances( + token_instance, + bob_eth_address, + alice_eth_address, + charlie_eth_address, + zeth_client.mixer_instance.address) + + # Bob tries to deposit ETHToken, split in 2 notes on the mixer (without + # approving) + try: + result_deposit_bob_to_bob = scenario.bob_deposit( + zeth_client, + mk_tree, + bob_eth_address, + keystore, + zeth.utils.EtherValue(0)) + except Exception as e: + allowance_mixer = allowance( + token_instance, + bob_eth_address, + zeth_client.mixer_instance.address) + print(f"[ERROR] Bob deposit failed! (msg: {e})") + print("The allowance for Mixer from Bob is: ", allowance_mixer) + + # Bob approves the transfer + print("- Bob approves the transfer of ETHToken to the Mixer") + tx_hash = approve( + token_instance, + bob_eth_address, + zeth_client.mixer_instance.address, + scenario.BOB_DEPOSIT_ETH) + eth.waitForTransactionReceipt(tx_hash) + allowance_mixer = allowance( + token_instance, + bob_eth_address, + zeth_client.mixer_instance.address) + print("- The allowance for the Mixer from Bob is:", allowance_mixer) + # Bob deposits ETHToken, split in 2 notes on the mixer + result_deposit_bob_to_bob = scenario.bob_deposit( + zeth_client, mk_tree, bob_eth_address, keystore) + + print("- Balances after Bob's deposit: ") + print_token_balances( + token_instance, + bob_eth_address, + alice_eth_address, + charlie_eth_address, + zeth_client.mixer_instance.address + ) + + # Alice sees a deposit and tries to decrypt the ciphertexts to see if she + # was the recipient, but Bob was the recipient so Alice fails to decrypt + received_notes = _receive_notes( + result_deposit_bob_to_bob.output_events) + recovered_notes_alice = received_notes['alice'] + assert(len(recovered_notes_alice) == 0), \ + "Alice decrypted a ciphertext that was not encrypted with her key!" + + # Bob does a transfer of ETHToken to Charlie on the mixer + + # Bob decrypts one of the note he previously received (useless here but + # useful if the payment came from someone else) + recovered_notes_bob = received_notes['bob'] + assert(len(recovered_notes_bob) == 2), \ + f"Bob recovered {len(recovered_notes_bob)} notes from deposit, expected 2" + input_bob_to_charlie = recovered_notes_bob[0].as_input() + + # Execution of the transfer + result_transfer_bob_to_charlie = scenario.bob_to_charlie( + zeth_client, + mk_tree, + input_bob_to_charlie, + bob_eth_address, + keystore) + + # Bob tries to spend `input_note_bob_to_charlie` twice + result_double_spending = None + try: + result_double_spending = scenario.bob_to_charlie( + zeth_client, + mk_tree, + input_bob_to_charlie, + bob_eth_address, + keystore) + except Exception as e: + print(f"Bob's double spending successfully rejected! (msg: {e})") + assert(result_double_spending is None), "Bob spent the same note twice!" + + print("- Balances after Bob's transfer to Charlie: ") + print_token_balances( + token_instance, + bob_eth_address, + alice_eth_address, + charlie_eth_address, + zeth_client.mixer_instance.address + ) + + # Charlie tries to decrypt the notes from Bob's previous transaction. + received_notes = _receive_notes( + result_transfer_bob_to_charlie.output_events) + note_descs_charlie = received_notes['charlie'] + assert(len(note_descs_charlie) == 1), \ + f"Charlie decrypted {len(note_descs_charlie)}. Expected 1!" + + _ = scenario.charlie_withdraw( + zeth_client, + mk_tree, + note_descs_charlie[0].as_input(), + charlie_eth_address, + keystore) + + print("- Balances after Charlie's withdrawal: ") + print_token_balances( + token_instance, + bob_eth_address, + alice_eth_address, + charlie_eth_address, + zeth_client.mixer_instance.address + ) + + # Charlie tries to carry out a double spend by withdrawing twice the same + # note + result_double_spending = None + try: + # New commitments are added in the tree at each withdraw so we + # recompute the path to have the updated nodes + result_double_spending = scenario.charlie_double_withdraw( + zeth_client, + mk_tree, + note_descs_charlie[0].as_input(), + charlie_eth_address, + keystore) + except Exception as e: + print(f"Charlie's double spending successfully rejected! (msg: {e})") + print("Balances after Charlie's double withdrawal attempt: ") + assert(result_double_spending is None), \ + "Charlie managed to withdraw the same note twice!" + print_token_balances( + token_instance, + bob_eth_address, + alice_eth_address, + charlie_eth_address, + zeth_client.mixer_instance.address) + + # Bob deposits once again ETH, split in 2 notes on the mixer + # But Charlie attempts to corrupt the transaction (malleability attack) + + # Bob approves the transfer + print("- Bob approves the transfer of ETHToken to the Mixer") + tx_hash = approve( + token_instance, + bob_eth_address, + zeth_client.mixer_instance.address, + scenario.BOB_DEPOSIT_ETH) + eth.waitForTransactionReceipt(tx_hash) + allowance_mixer = allowance( + token_instance, + bob_eth_address, + zeth_client.mixer_instance.address) + print("- The allowance for the Mixer from Bob is:", allowance_mixer) + + result_deposit_bob_to_bob = scenario.charlie_corrupt_bob_deposit( + zeth_client, + mk_tree, + bob_eth_address, + charlie_eth_address, + keystore) + + # Bob decrypts one of the note he previously received (should fail if + # Charlie's attack succeeded) + received_notes = _receive_notes( + result_deposit_bob_to_bob.output_events) + recovered_notes_bob = received_notes['bob'] + assert(len(recovered_notes_bob) == 2), \ + f"Bob recovered {len(recovered_notes_bob)} notes from deposit, expected 2" + + print("- Balances after Bob's last deposit: ") + print_token_balances( + token_instance, + bob_eth_address, + alice_eth_address, + charlie_eth_address, + zeth_client.mixer_instance.address) + + print( + "========================================\n" + + " TESTS PASSED\n" + + "========================================\n") + + +if __name__ == '__main__': + main() diff --git a/test_commands/test_ether_mixing.py b/test_commands/test_ether_mixing.py new file mode 100644 index 0000000..70ddef1 --- /dev/null +++ b/test_commands/test_ether_mixing.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +import zeth.constants +import zeth.contracts +import zeth.merkle_tree +import zeth.utils +import zeth.zksnark +from zeth.zeth_address import ZethAddressPriv +from zeth.contracts import MixOutputEvents +from zeth.mixer_client import MixerClient +from zeth.wallet import Wallet, ZethNoteDescription +import test_commands.mock as mock +import test_commands.scenario as scenario + +from os.path import join, exists +import shutil +from typing import Dict, List, Any + + +def print_balances( + web3: Any, bob: str, alice: str, charlie: str, mixer: str) -> None: + print("BALANCES:") + print(f" Alice : {web3.eth.getBalance(alice)}") + print(f" Bob : {web3.eth.getBalance(bob)}") + print(f" Charlie : {web3.eth.getBalance(charlie)}") + print(f" Mixer : {web3.eth.getBalance(mixer)}") + + +def main() -> None: + zksnark = zeth.zksnark.get_zksnark_provider(zeth.utils.parse_zksnark_arg()) + + web3, eth = mock.open_test_web3() + + # Zeth addresses + keystore = mock.init_test_keystore() + # Ethereum addresses + deployer_eth_address = eth.accounts[0] + bob_eth_address = eth.accounts[1] + alice_eth_address = eth.accounts[2] + charlie_eth_address = eth.accounts[3] + + # Deploy Zeth contracts + tree_depth = zeth.constants.ZETH_MERKLE_TREE_DEPTH + zeth_client, _contract_desc = MixerClient.deploy( + web3, + mock.TEST_PROVER_SERVER_ENDPOINT, + deployer_eth_address, + None, + None, + zksnark) + + # Set up Merkle tree and Wallets. Note that each wallet holds an internal + # Merkle Tree, unused in this test. Instead, we keep an in-memory version + # shared by all virtual users. This avoids having to pass all mix results + # to all wallets, and allows some of the methods in the scenario module, + # which must update the tree directly. + mk_tree = zeth.merkle_tree.MerkleTree.empty_with_depth(tree_depth) + mixer_instance = zeth_client.mixer_instance + + # Keys and wallets + def _mk_wallet(name: str, sk: ZethAddressPriv) -> Wallet: + wallet_dir = join(mock.TEST_NOTE_DIR, name + "-eth") + if exists(wallet_dir): + # Note: symlink-attack resistance + # https://docs.python.org/3/library/shutil.html#shutil.rmtree.avoids_symlink_attacks + shutil.rmtree(wallet_dir) + return Wallet(mixer_instance, name, wallet_dir, sk) + sk_alice = keystore['Alice'].addr_sk + sk_bob = keystore['Bob'].addr_sk + sk_charlie = keystore['Charlie'].addr_sk + alice_wallet = _mk_wallet('alice', sk_alice) + bob_wallet = _mk_wallet('bob', sk_bob) + charlie_wallet = _mk_wallet('charlie', sk_charlie) + block_num = 1 + + # Universal update function + def _receive_notes( + out_ev: List[MixOutputEvents]) \ + -> Dict[str, List[ZethNoteDescription]]: + nonlocal block_num + notes = { + 'alice': alice_wallet.receive_notes(out_ev), + 'bob': bob_wallet.receive_notes(out_ev), + 'charlie': charlie_wallet.receive_notes(out_ev), + } + alice_wallet.update_and_save_state(block_num) + bob_wallet.update_and_save_state(block_num) + charlie_wallet.update_and_save_state(block_num) + block_num = block_num + 1 + return notes + + print("[INFO] 4. Running tests (asset mixed: Ether)...") + print("- Initial balances: ") + print_balances( + web3, + bob_eth_address, + alice_eth_address, + charlie_eth_address, + zeth_client.mixer_instance.address) + + # Bob deposits ETH, split in 2 notes on the mixer + result_deposit_bob_to_bob = scenario.bob_deposit( + zeth_client, + mk_tree, + bob_eth_address, + keystore) + + print("- Balances after Bob's deposit: ") + print_balances( + web3, + bob_eth_address, + alice_eth_address, + charlie_eth_address, + zeth_client.mixer_instance.address + ) + + # Alice sees a deposit and tries to decrypt the ciphertexts to see if she + # was the recipient but she wasn't the recipient (Bob was), so she fails to + # decrypt + recovered_notes = _receive_notes(result_deposit_bob_to_bob.output_events) + assert(len(recovered_notes['alice']) == 0), \ + "Alice decrypted a ciphertext that was not encrypted with her key!" + + # Bob does a transfer to Charlie on the mixer + + # Bob decrypts one of the note he previously received (useless here but + # useful if the payment came from someone else) + assert(len(recovered_notes['bob']) == 2), \ + f"Bob recovered {len(recovered_notes['bob'])} notes, expected 2" + + # Execution of the transfer + result_transfer_bob_to_charlie = scenario.bob_to_charlie( + zeth_client, + mk_tree, + recovered_notes['bob'][0].as_input(), + bob_eth_address, + keystore) + + # Bob tries to spend `input_note_bob_to_charlie` twice + result_double_spending = None + try: + result_double_spending = scenario.bob_to_charlie( + zeth_client, + mk_tree, + recovered_notes['bob'][0].as_input(), + bob_eth_address, + keystore) + except Exception as e: + print(f"Bob's double spending successfully rejected! (msg: {e})") + assert(result_double_spending is None), \ + "Bob managed to spend the same note twice!" + + print("- Balances after Bob's transfer to Charlie: ") + print_balances( + web3, + bob_eth_address, + alice_eth_address, + charlie_eth_address, + zeth_client.mixer_instance.address + ) + + # Charlie recovers his notes and attempts to withdraw them. + recovered_notes = _receive_notes( + result_transfer_bob_to_charlie.output_events) + notes_charlie = recovered_notes['charlie'] + assert(len(notes_charlie) == 1), \ + f"Charlie decrypted {len(notes_charlie)}. Expected 1!" + + input_charlie_withdraw = notes_charlie[0] + + charlie_balance_before_withdrawal = eth.getBalance(charlie_eth_address) + _ = scenario.charlie_withdraw( + zeth_client, + mk_tree, + input_charlie_withdraw.as_input(), + charlie_eth_address, + keystore) + charlie_balance_after_withdrawal = eth.getBalance(charlie_eth_address) + print("Balances after Charlie's withdrawal: ") + print_balances( + web3, + bob_eth_address, + alice_eth_address, + charlie_eth_address, + zeth_client.mixer_instance.address) + if charlie_balance_after_withdrawal <= charlie_balance_before_withdrawal: + raise Exception("Charlie's balance did not increase after withdrawal") + + # Charlie tries to double-spend by withdrawing twice the same note + result_double_spending = None + try: + # New commitments are added in the tree at each withdraw so we + # recompiute the path to have the updated nodes + result_double_spending = scenario.charlie_double_withdraw( + zeth_client, + mk_tree, + input_charlie_withdraw.as_input(), + charlie_eth_address, + keystore) + except Exception as e: + print(f"Charlie's double spending successfully rejected! (msg: {e})") + print("Balances after Charlie's double withdrawal attempt: ") + assert(result_double_spending is None), \ + "Charlie managed to withdraw the same note twice!" + print_balances( + web3, + bob_eth_address, + alice_eth_address, + charlie_eth_address, + zeth_client.mixer_instance.address) + + # Bob deposits once again ETH, split in 2 notes on the mixer + # But Charlie attempts to corrupt the transaction (malleability attack) + result_deposit_bob_to_bob = scenario.charlie_corrupt_bob_deposit( + zeth_client, + mk_tree, + bob_eth_address, + charlie_eth_address, + keystore) + + # Bob decrypts one of the note he previously received (should fail if + # Charlie's attack succeeded) + recovered_notes = _receive_notes( + result_deposit_bob_to_bob.output_events) + assert(len(recovered_notes['bob']) == 2), \ + f"Bob recovered {len(recovered_notes['bob'])} notes, expected 2" + + print("- Balances after Bob's last deposit: ") + print_balances( + web3, + bob_eth_address, + alice_eth_address, + charlie_eth_address, + zeth_client.mixer_instance.address) + + print( + "========================================\n" + + " TESTS PASSED\n" + + "========================================\n") + + +if __name__ == '__main__': + main() diff --git a/test_commands/test_merkle_tree_contract.py b/test_commands/test_merkle_tree_contract.py new file mode 100644 index 0000000..e3be7b3 --- /dev/null +++ b/test_commands/test_merkle_tree_contract.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from zeth.constants import ZETH_MERKLE_TREE_DEPTH +from zeth.merkle_tree import MerkleTree +from zeth.utils import extend_32bytes +from typing import List, Any +import test_commands.mock as mock + + +TEST_VALUES = [ + extend_32bytes(bytes.fromhex("f0")), + extend_32bytes(bytes.fromhex("f1")), + extend_32bytes(bytes.fromhex("f2")), + extend_32bytes(bytes.fromhex("f3")), + extend_32bytes(bytes.fromhex("f4")), + extend_32bytes(bytes.fromhex("f5")), + extend_32bytes(bytes.fromhex("f6")), + extend_32bytes(bytes.fromhex("f7")), + extend_32bytes(bytes.fromhex("f8")), + extend_32bytes(bytes.fromhex("f9")), + extend_32bytes(bytes.fromhex("fa")), + extend_32bytes(bytes.fromhex("fb")), + extend_32bytes(bytes.fromhex("fc")), + extend_32bytes(bytes.fromhex("fd")), + extend_32bytes(bytes.fromhex("fe")), + extend_32bytes(bytes.fromhex("ff")), +] + + +def assert_root(expect_root: bytes, nodes: List[bytes], msg: str) -> None: + if nodes[0] != expect_root: + print(f"FAILED: {msg}") + print(f"Expected: {expect_root.hex()}") + print("Actual :") + for layer_idx in range(0, ZETH_MERKLE_TREE_DEPTH + 1): + layer_size = pow(2, layer_idx) + layer_start = layer_size - 1 + layer = nodes[layer_start:layer_start + layer_size] + layer_hex = [node.hex() for node in layer] + print(f" {layer_hex}") + raise Exception(f"failed") + + +def test_tree_empty(contract: Any) -> None: + mktree = MerkleTree.empty_with_depth(ZETH_MERKLE_TREE_DEPTH) + expect_root = mktree.recompute_root() + nodes = contract.functions.testAddLeaves([], []).call() + assert_root(expect_root, nodes, "test_tree_empty") + + +def test_tree_partial(contract: Any) -> None: + """ + Send a series of different arrays of leaves to the contract and check that + the root is as expected. Send as 2 batches, to test updating the tree, from + various states. + """ + + def _test_partial(num_entries: int, step: int = 1) -> None: + """ + Take the first 'num_entries' from TEST_VALUES. Cut them at each possible + place and submit them as two halves to the contract, receiving back the + set of nodes. + """ + leaves = TEST_VALUES[:num_entries] + + mktree = MerkleTree.empty_with_depth(ZETH_MERKLE_TREE_DEPTH) + for leaf in leaves: + mktree.insert(leaf) + expect_root = mktree.recompute_root() + + for cut in range(0, num_entries + 1, step): + print(f"_test_partial: num_entries={num_entries}, cut={cut}") + first = leaves[:cut] + second = leaves[cut:] + nodes = contract.functions.testAddLeaves(first, second).call() + assert_root( + expect_root, + nodes, + f"num_entries: {num_entries}, cut: {cut}: ") + + # Perform the filling tests using arrays of these sizes + _test_partial(1) + _test_partial(7) + _test_partial(8) + _test_partial(9) + _test_partial(15, 3) + _test_partial(16, 3) + + +def main() -> None: + _web3, eth = mock.open_test_web3() + deployer_eth_address = eth.accounts[0] + _mktree_interface, mktree_instance = mock.deploy_contract( + eth, + deployer_eth_address, + "MerkleTreeMiMC7_test", + {'treeDepth': ZETH_MERKLE_TREE_DEPTH}) + + test_tree_empty(mktree_instance) + test_tree_partial(mktree_instance) + + +if __name__ == '__main__': + main() diff --git a/zeth/__init__.py b/zeth/__init__.py new file mode 100644 index 0000000..6fb9229 --- /dev/null +++ b/zeth/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ diff --git a/zeth/constants.py b/zeth/constants.py new file mode 100644 index 0000000..1869918 --- /dev/null +++ b/zeth/constants.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +""" +Constants used by zeth. By convention lengths are given in bits as +`*_LENGTH` and the corresponding `*_LENGTH_BYTES` variable holds the size in +bytes (where this is meaningful). +""" + +from typing import List + + +# Defined here instead of utils.py to avoid circular imports, since utils.py +# depends on some of the values defined here. +def bit_length_to_byte_length(bit_length: int) -> int: + """ + Convert bit length to byte length + """ + assert \ + bit_length >= 0 and bit_length % 8 == 0, \ + "Not valid bit_length inserted" + return int(bit_length/8) + + +# GROTH16 constants +GROTH16_ZKSNARK: str = "GROTH16" +GROTH16_MIXER_CONTRACT: str = "Groth16Mixer" + +# PGHR13 constants +PGHR13_ZKSNARK: str = "PGHR13" +PGHR13_MIXER_CONTRACT: str = "Pghr13Mixer" + +# Set of valid snarks +VALID_ZKSNARKS: List[str] = [GROTH16_ZKSNARK, PGHR13_ZKSNARK] + +# Default zk-snark +ZKSNARK_DEFAULT: str = GROTH16_ZKSNARK + +# Merkle tree depth +ZETH_MERKLE_TREE_DEPTH: int = 32 + +# Nb of input notes +JS_INPUTS: int = 2 + +# Nb of output notes +JS_OUTPUTS: int = 2 + +# Gas cost estimates +DEPLOYMENT_GAS_WEI: int = ZETH_MERKLE_TREE_DEPTH * 250000 + +DEFAULT_MIX_GAS_WEI: int = DEPLOYMENT_GAS_WEI + +# Order of the largest prime order subgroup of the elliptic curve group. See: +# https://github.com/ethereum/go-ethereum/blob/master/crypto/bn256/cloudflare/constants.go#L23 +# # noqa +ZETH_PRIME: int = \ + 21888242871839275222246405745257275088548364400416034343698204186575808495617 + +# Field capacity (=floor(log_2(ZETH_PRIME))) +FIELD_CAPACITY: int = 253 + +# Hash digest length (for commitment and PRFs) +DIGEST_LENGTH: int = 256 + +# Public value length (v_pub_in and v_pub_out) +PUBLIC_VALUE_LENGTH: int = 64 +PUBLIC_VALUE_LENGTH_BYTES: int = bit_length_to_byte_length(PUBLIC_VALUE_LENGTH) +PUBLIC_VALUE_MASK: int = (1 << PUBLIC_VALUE_LENGTH) - 1 + +# Number of residual bits when encoding digests into field values +DIGEST_RESIDUAL_BITS: int = max(0, DIGEST_LENGTH - FIELD_CAPACITY) + +PHI_LENGTH: int = 256 +PHI_LENGTH_BYTES: int = bit_length_to_byte_length(PHI_LENGTH) + +APK_LENGTH: int = 256 +APK_LENGTH_BYTES: int = bit_length_to_byte_length(APK_LENGTH) + +RHO_LENGTH: int = 256 +RHO_LENGTH_BYTES: int = bit_length_to_byte_length(RHO_LENGTH) + +TRAPR_LENGTH: int = 256 +TRAPR_LENGTH_BYTES: int = bit_length_to_byte_length(TRAPR_LENGTH) + +NOTE_LENGTH: int = APK_LENGTH + PUBLIC_VALUE_LENGTH + RHO_LENGTH + TRAPR_LENGTH +NOTE_LENGTH_BYTES: int = bit_length_to_byte_length(NOTE_LENGTH) + +# Public inputs are (see BaseMixer.sol): +# [0 ] - 1 x merkle root +# [1 ] - jsOut x commitment +# [1 + jsOut ] - jsIn x nullifier (partial) +# [1 + jsOut + jsIn ] - 1 x hsig (partial) +# [2 + jsOut + jsIn ] - JsIn x message auth tags (partial) +# [2 + jsOut + 2*jsIn] - 1 x residual bits, v_in, v_out + +# Index (in public inputs) of residual bits +RESIDUAL_BITS_INDEX: int = (2 * JS_INPUTS) + JS_OUTPUTS + 2 + +# Number of full-length digests to be encoded in public inputs +NUM_INPUT_DIGESTS: int = (2 * JS_INPUTS) + 1 + +# Total number of residual bits corresponding to digests in public inputs +TOTAL_DIGEST_RESIDUAL_BITS: int = NUM_INPUT_DIGESTS * DIGEST_RESIDUAL_BITS + +# Solidity compiler version +SOL_COMPILER_VERSION: str = 'v0.5.16' + +# Seed for MIMC +MIMC_MT_SEED: str = "clearmatics_mt_seed" + +# Units for vpub_in and vpub_out, given in Wei. i.e. +# Value (in Wei) = vpub_{in,out} * ZETH_PUBLIC_UNIT_VALUE +ZETH_PUBLIC_UNIT_VALUE: int = 1000000000000 # 1 Szabo (10^12 Wei). diff --git a/zeth/contracts.py b/zeth/contracts.py new file mode 100644 index 0000000..4cb4676 --- /dev/null +++ b/zeth/contracts.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from __future__ import annotations +from zeth.signing import SigningVerificationKey, Signature, \ + verification_key_as_mix_parameter, verification_key_from_mix_parameter, \ + signature_as_mix_parameter, signature_from_mix_parameter +from zeth.zksnark import IZKSnarkProvider, GenericProof +from zeth.utils import EtherValue, hex_to_int +from zeth.constants import SOL_COMPILER_VERSION +import json +import solcx +import traceback +from typing import Dict, List, Iterator, Optional, Any + +# Avoid trying to read too much data into memory +SYNC_BLOCKS_PER_BATCH = 10 + +Interface = Dict[str, Any] + + +class MixParameters: + """ + Arguments to the mix call. + """ + def __init__( + self, + extended_proof: GenericProof, + signature_vk: SigningVerificationKey, + signature: Signature, + ciphertexts: List[bytes]): + self.extended_proof = extended_proof + self.signature_vk = signature_vk + self.signature = signature + self.ciphertexts = ciphertexts + + @staticmethod + def from_json(params_json: str) -> MixParameters: + return MixParameters._from_json_dict(json.loads(params_json)) + + def to_json(self) -> str: + return json.dumps(self._to_json_dict()) + + def _to_json_dict(self) -> Dict[str, Any]: + signature_vk_json = [ + str(x) for x in verification_key_as_mix_parameter(self.signature_vk)] + signature_json = str(signature_as_mix_parameter(self.signature)) + ciphertexts_json = [x.hex() for x in self.ciphertexts] + return { + "extended_proof": self.extended_proof, + "signature_vk": signature_vk_json, + "signature": signature_json, + "ciphertexts": ciphertexts_json, + } + + @staticmethod + def _from_json_dict(json_dict: Dict[str, Any]) -> MixParameters: + ext_proof = json_dict["extended_proof"] + signature_pk_param = [int(x) for x in json_dict["signature_vk"]] + signature_pk = verification_key_from_mix_parameter(signature_pk_param) + signature = signature_from_mix_parameter(int(json_dict["signature"])) + ciphertexts = [bytes.fromhex(x) for x in json_dict["ciphertexts"]] + return MixParameters( + ext_proof, signature_pk, signature, ciphertexts) + + +class MixOutputEvents: + """ + Event data for a single joinsplit output. Holds address (in merkle tree), + commitment and ciphertext. + """ + def __init__( + self, commitment: bytes, ciphertext: bytes): + self.commitment = commitment + self.ciphertext = ciphertext + + +class MixResult: + """ + Data structure representing the result of the mix call. + """ + def __init__( + self, + new_merkle_root: bytes, + nullifiers: List[bytes], + output_events: List[MixOutputEvents]): + self.new_merkle_root = new_merkle_root + self.nullifiers = nullifiers + self.output_events = output_events + + +def _event_args_to_mix_result(event_args: Any) -> MixResult: + mix_out_args = zip(event_args.commitments, event_args.ciphertexts) + out_events = [MixOutputEvents(c, ciph) for (c, ciph) in mix_out_args] + return MixResult( + new_merkle_root=event_args.root, + nullifiers=event_args.nullifiers, + output_events=out_events) + + +class InstanceDescription: + """ + Minimal data required to instantiate the in-memory interface to a contract. + """ + def __init__(self, address: str, abi: Dict[str, Any]): + self.address = address + self.abi = abi + + def to_json_dict(self) -> Dict[str, Any]: + return { + "address": self.address, + "abi": self.abi + } + + @staticmethod + def from_json_dict(desc_json: Dict[str, Any]) -> InstanceDescription: + return InstanceDescription(desc_json["address"], desc_json["abi"]) + + @staticmethod + def deploy( + web3: Any, + source_file: str, + contract_name: str, + deployer_address: str, + deployment_gas: EtherValue, + compiler_flags: Dict[str, Any] = None, + **kwargs: Any) -> InstanceDescription: + """ + Compile and deploy a contract, returning the live instance and an instance + description (which the caller should save in order to access the + instance in the future). + """ + compiled = InstanceDescription.compile( + source_file, contract_name, compiler_flags) + assert compiled + instance_desc = InstanceDescription.deploy_from_compiled( + web3, deployer_address, deployment_gas, compiled, **kwargs) + print( + f"deploy: contract: {contract_name} " + f"to address: {instance_desc.address}") + return instance_desc + + @staticmethod + def deploy_from_compiled( + web3: Any, + deployer_address: str, + deployment_gas: EtherValue, + compiled: Any, + **kwargs: Any) -> InstanceDescription: + contract = web3.eth.contract( + abi=compiled['abi'], bytecode=compiled['bin']) + tx_hash = contract.constructor(**kwargs).transact({ + 'from': deployer_address, + 'gas': deployment_gas.wei + }) + tx_receipt = web3.eth.waitForTransactionReceipt(tx_hash, 10000) + contract_address = tx_receipt['contractAddress'] + print( + f"deploy: tx_hash={tx_hash[0:8].hex()}, " + + f" gasUsed={tx_receipt.gasUsed}, status={tx_receipt.status}") + return InstanceDescription(contract_address, compiled['abi']) + + @staticmethod + def compile( + source_file: str, + contract_name: str, + compiler_flags: Dict[str, Any] = None) \ + -> Any: + compiled_all = compile_files([source_file], **(compiler_flags or {})) + assert compiled_all + compiled = compiled_all[f"{source_file}:{contract_name}"] + assert compiled + return compiled + + def instantiate(self, web3: Any) -> Any: + """ + Return the instantiated contract + """ + return web3.eth.contract(address=self.address, abi=self.abi) + + +def get_block_number(web3: Any) -> int: + return web3.eth.blockNumber + + +def install_sol() -> None: + solcx.install_solc(SOL_COMPILER_VERSION) + + +def compile_files(files: List[str], **kwargs: Any) -> Any: + """ + Wrapper around solcx which ensures the required version of the compiler is + used. + """ + solcx.set_solc_version(SOL_COMPILER_VERSION) + return solcx.compile_files(files, optimize=True, **kwargs) + + +def mix_parameters_as_contract_arguments( + zksnark: IZKSnarkProvider, + mix_parameters: MixParameters) -> List[Any]: + """ + Convert MixParameters to a list of eth ABI objects which can be passed to + the contract's mix method. + """ + proof_params: List[Any] = zksnark.mixer_proof_parameters( + mix_parameters.extended_proof) + proof_params.extend([ + verification_key_as_mix_parameter(mix_parameters.signature_vk), + signature_as_mix_parameter(mix_parameters.signature), + hex_to_int(mix_parameters.extended_proof["inputs"]), + mix_parameters.ciphertexts + ]) + return proof_params + + +def _create_web3_mixer_call( + zksnark: IZKSnarkProvider, + mixer_instance: Any, + mix_parameters: MixParameters) -> Any: + mix_params_eth = mix_parameters_as_contract_arguments(zksnark, mix_parameters) + return mixer_instance.functions.mix(*mix_params_eth) + + +def mix_call( + zksnark: IZKSnarkProvider, + mixer_instance: Any, + mix_parameters: MixParameters, + sender_address: str, + wei_pub_value: int, + call_gas: int) -> bool: + """ + Call the mix method (executes on the RPC host, without creating a + transaction). Returns True if the call succeeds. False, otherwise. + """ + mixer_call = _create_web3_mixer_call(zksnark, mixer_instance, mix_parameters) + try: + mixer_call.call({ + 'from': sender_address, + 'value': wei_pub_value, + 'gas': call_gas + }) + return True + + except ValueError: + print("error executing mix call:") + traceback.print_exc() + + return False + + +def mix( + zksnark: IZKSnarkProvider, + mixer_instance: Any, + mix_parameters: MixParameters, + sender_address: str, + wei_pub_value: int, + call_gas: int) -> str: + """ + Create and broadcast a transaction that calls the mix method of the Mixer + """ + mixer_call = _create_web3_mixer_call(zksnark, mixer_instance, mix_parameters) + tx_hash = mixer_call.transact({ + 'from': sender_address, + 'value': wei_pub_value, + 'gas': call_gas + }) + return tx_hash.hex() + + +def parse_mix_call( + mixer_instance: Any, + _tx_receipt: str) -> MixResult: + """ + Get the logs data associated with this mixing + """ + log_mix_filter = mixer_instance.eventFilter("LogMix", {'fromBlock': 'latest'}) + log_mix_events = log_mix_filter.get_all_entries() + mix_results = [_event_args_to_mix_result(ev.args) for ev in log_mix_events] + return mix_results[0] + + +def _next_nullifier_or_none(nullifier_iter: Iterator[bytes]) -> Optional[Any]: + try: + return next(nullifier_iter) + except StopIteration: + return None + + +def get_mix_results( + web3: Any, + mixer_instance: Any, + start_block: int, + end_block: int) -> Iterator[MixResult]: + """ + Iterator for all events generated by 'mix' executions, over some block + range. Batches eth RPC calls to avoid holding huge numbers of events in + memory. + """ + for batch_start in range(start_block, end_block + 1, SYNC_BLOCKS_PER_BATCH): + # Get mk_root, address and ciphertext filters for + try: + filter_params = { + 'fromBlock': batch_start, + 'toBlock': batch_start + SYNC_BLOCKS_PER_BATCH - 1, + } + log_mix_filter = mixer_instance.eventFilter("LogMix", filter_params) + for log_mix_event in log_mix_filter.get_all_entries(): + yield _event_args_to_mix_result(log_mix_event.args) + + finally: + web3.eth.uninstallFilter(log_mix_filter.filter_id) diff --git a/zeth/encryption.py b/zeth/encryption.py new file mode 100644 index 0000000..ec7c02f --- /dev/null +++ b/zeth/encryption.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +""" +Encryption operations for Zeth notes. Supports an `encrypt` operation using +receivers public key, and a `decrypt` operation using the corresponding private +key. `decrypt` fails (except with negligible probability) if the ciphertext was +encrypted with a different public key. + +This implementation makes use of the `cryptography` library with OpenSSL +backend. For the avoidance of doubt, the implementation adheres to the +appropriate standards as follows. (links refer to specific versions of external +libraries, to ensure that line numbers are correct, but the descriptions are +expected to hold for all versions.) + +As described in [Bernstein06], private keys may be generated as 32 random bytes +with bits 0, 1 and 2 of the first byte cleared, bit 7 of the last byte cleared, +and bit 6 of the last byte set. This happens at key generation time. See: + + https://github.com/openssl/openssl/blob/be9d82bb35812ac65cd92316d1ae7c7c75efe9cf/crypto/ec/ecx_meth.c#L81 + +[LangleyN18] describes Poly1305, including the requirement that the "r" value of +the key (r, s) be "clamped". Note that this clamping is carried out by the +cryptography library when the key is generated. See: + + https://github.com/openssl/openssl/blob/master/crypto/poly1305/poly1305.c#L143 + +The specification of the ChaCha20 stream cipher in [LangleyN18] (page 10) +describes the inputs to the encryption functions as a 256-bit key, a 32-bit +counter and a 96-bit nonce. This differs slightly from the signature of the +encryption function in the cryptography library, which accepts a 256-bit key and +128-bit nonce. That is, no counter is mentioned leaving ambiguity as to whether +this data is processed exactly as described in [LangleyN18]. Internally, the +cryptography library treats the first 32-bit word of the nonce as a counter and +increments this as necessary in accordance with [LangleyN18]. See: + + https://github.com/openssl/openssl/blob/be9d82bb35812ac65cd92316d1ae7c7c75efe9cf/crypto/chacha/chacha_enc.c#L128 + https://github.com/openssl/openssl/blob/be9d82bb35812ac65cd92316d1ae7c7c75efe9cf/crypto/evp/e_chacha20_poly1305.c#L95 + +References: + +\\[Bernstein06] + "Curve25519:new Diffie-Hellman speed records" + Daniel J. Bernstein, + International Workshop on Public Key Cryptography, 2006, + + +\\[LangleyN18] + "Chacha20 and poly1305 for ietf protocols." + Adam Langley and Yoav Nir, + RFC 8439, 2018, + +""" + +from zeth.constants import NOTE_LENGTH_BYTES, bit_length_to_byte_length +from cryptography.hazmat.primitives.asymmetric.x25519 \ + import X25519PrivateKey, X25519PublicKey +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, poly1305 +from cryptography.hazmat.primitives.serialization import \ + Encoding, PrivateFormat, PublicFormat, NoEncryption +from cryptography.exceptions import InvalidSignature \ + as cryptography_InvalidSignature +from typing import Tuple, NewType + + +# Internal sizes for the scheme +_SYM_KEY_LENGTH: int = 256 +_SYM_KEY_LENGTH_BYTES: int = bit_length_to_byte_length(_SYM_KEY_LENGTH) + +_MAC_KEY_LENGTH: int = 256 +_MAC_KEY_LENGTH_BYTES: int = bit_length_to_byte_length(_MAC_KEY_LENGTH) + +_KEY_MATERIAL_LENGTH_BYTES: int = _SYM_KEY_LENGTH_BYTES + _MAC_KEY_LENGTH_BYTES + +_TAG_LENGTH: int = 128 +_TAG_LENGTH_BYTES = bit_length_to_byte_length(_TAG_LENGTH) + +_SYM_NONCE_LENGTH: int = 128 +_SYM_NONCE_LENGTH_BYTES: int = bit_length_to_byte_length(_SYM_NONCE_LENGTH) + +# Nonce as 4 32-bit words [counter, nonce, nonce, nonce] (see above). +_SYM_NONCE_VALUE: bytes = b"\x00" * _SYM_NONCE_LENGTH_BYTES + +# Key Derivation Tag "ZethEnc" utf-8 encoding +_KDF_TAG: bytes = b'ZethEnc' + +# Public sizes +EC_PRIVATE_KEY_LENGTH: int = 256 +EC_PUBLIC_KEY_LENGTH: int = 256 +EC_PUBLIC_KEY_LENGTH_BYTES: int = bit_length_to_byte_length(EC_PUBLIC_KEY_LENGTH) +ENCRYPTED_NOTE_LENGTH_BYTES: int = \ + EC_PUBLIC_KEY_LENGTH_BYTES + NOTE_LENGTH_BYTES + _TAG_LENGTH_BYTES + +# Expose the exception type +InvalidSignature = cryptography_InvalidSignature + +# Represents a secret key for encryption +EncryptionSecretKey = NewType('EncryptionSecretKey', object) + + +def generate_encryption_secret_key() -> EncryptionSecretKey: + return EncryptionSecretKey(X25519PrivateKey.generate()) # type: ignore + + +def encode_encryption_secret_key(sk: EncryptionSecretKey) -> bytes: + return sk.private_bytes( # type: ignore + Encoding.Raw, PrivateFormat.Raw, NoEncryption()) + + +def decode_encryption_secret_key(sk_bytes: bytes) -> EncryptionSecretKey: + return EncryptionSecretKey( + X25519PrivateKey.from_private_bytes(sk_bytes)) + + +def encryption_secret_key_as_hex(sk: EncryptionSecretKey) -> str: + return encode_encryption_secret_key(sk).hex() # type: ignore + + +def encryption_secret_key_from_hex(pk_str: str) -> EncryptionSecretKey: + return EncryptionSecretKey( + X25519PrivateKey.from_private_bytes(bytes.fromhex(pk_str))) + + +# Public key for decryption +EncryptionPublicKey = NewType('EncryptionPublicKey', object) + + +def get_encryption_public_key( + enc_secret: EncryptionSecretKey) -> EncryptionPublicKey: + return enc_secret.public_key() # type: ignore + + +def encode_encryption_public_key(pk: EncryptionPublicKey) -> bytes: + return pk.public_bytes(Encoding.Raw, PublicFormat.Raw) # type: ignore + + +def decode_encryption_public_key(pk_data: bytes) -> EncryptionPublicKey: + return EncryptionPublicKey(X25519PublicKey.from_public_bytes(pk_data)) + + +def encryption_public_key_as_hex(pk: EncryptionPublicKey) -> str: + return encode_encryption_public_key(pk).hex() + + +def encryption_public_key_from_hex(pk_str: str) -> EncryptionPublicKey: + return decode_encryption_public_key(bytes.fromhex(pk_str)) + + +class EncryptionKeyPair: + """ + Key-pair for encrypting joinsplit notes. + """ + def __init__(self, k_sk: EncryptionSecretKey, k_pk: EncryptionPublicKey): + self.k_pk: EncryptionPublicKey = k_pk + self.k_sk: EncryptionSecretKey = k_sk + + +def generate_encryption_keypair() -> EncryptionKeyPair: + sk = generate_encryption_secret_key() + return EncryptionKeyPair(sk, get_encryption_public_key(sk)) + + +def encrypt(message: bytes, pk_receiver: EncryptionPublicKey) -> bytes: + """ + Encrypts a string message under a ec25519 public key by using a custom + dhaes-based scheme. See: https://eprint.iacr.org/1999/007 + """ + assert \ + len(message) == NOTE_LENGTH_BYTES, \ + f"expected message length {NOTE_LENGTH_BYTES}, saw {len(message)}" + + # Generate ephemeral keypair + eph_keypair = generate_encryption_keypair() + + # Compute shared secret and eph key + shared_key = _exchange(eph_keypair.k_sk, pk_receiver) + pk_sender_bytes = encode_encryption_public_key(eph_keypair.k_pk) + + # Generate key material + sym_key, mac_key = _kdf(pk_sender_bytes, shared_key) + + # Generate symmetric ciphertext + # Chacha encryption + algorithm = algorithms.ChaCha20(sym_key, _SYM_NONCE_VALUE) + cipher = Cipher(algorithm, mode=None, backend=default_backend()) + encryptor = cipher.encryptor() + sym_ciphertext = encryptor.update(message) + + # Generate mac + mac = poly1305.Poly1305(mac_key) + mac.update(sym_ciphertext) + tag = mac.finalize() + + # Arrange ciphertext + return pk_sender_bytes+sym_ciphertext+tag + + +def decrypt( + encrypted_message: bytes, + sk_receiver: EncryptionSecretKey) -> bytes: + """ + Decrypts a NOTE_LENGTH-byte message by using valid ec25519 private key + objects. See: https://pynacl.readthedocs.io/en/stable/public/ + """ + assert \ + len(encrypted_message) == ENCRYPTED_NOTE_LENGTH_BYTES, \ + "encrypted_message byte-length must be: "+str(ENCRYPTED_NOTE_LENGTH_BYTES) + + assert(isinstance(sk_receiver, X25519PrivateKey)), \ + f"PrivateKey: {sk_receiver} ({type(sk_receiver)})" + + # Compute shared secret + pk_sender_bytes = encrypted_message[:EC_PUBLIC_KEY_LENGTH_BYTES] + pk_sender = decode_encryption_public_key(pk_sender_bytes) + shared_key = _exchange(sk_receiver, pk_sender) + + # Generate key material and recover keys + sym_key, mac_key = _kdf(pk_sender_bytes, shared_key) + + # ct_sym and mac + ct_sym = encrypted_message[ + EC_PUBLIC_KEY_LENGTH_BYTES: + EC_PUBLIC_KEY_LENGTH_BYTES + NOTE_LENGTH_BYTES] + tag = encrypted_message[ + EC_PUBLIC_KEY_LENGTH_BYTES + NOTE_LENGTH_BYTES: + EC_PUBLIC_KEY_LENGTH_BYTES + NOTE_LENGTH_BYTES + _TAG_LENGTH_BYTES] + + # Verify the mac + mac = poly1305.Poly1305(mac_key) + mac.update(ct_sym) + mac.verify(tag) + + # Decrypt sym ciphertext + algorithm = algorithms.ChaCha20(sym_key, _SYM_NONCE_VALUE) + cipher = Cipher(algorithm, mode=None, backend=default_backend()) + decryptor = cipher.decryptor() + message = decryptor.update(ct_sym) + + return message + + +def _exchange(sk: EncryptionSecretKey, pk: EncryptionPublicKey) -> bytes: + return sk.exchange(pk) # type: ignore + + +def _kdf(eph_pk: bytes, shared_key: bytes) -> Tuple[bytes, bytes]: + """ + Key derivation function + """ + # Hashing + key_material = hashes.Hash( + hashes.BLAKE2b(64), + backend=default_backend()) + key_material.update(_KDF_TAG) + key_material.update(eph_pk) + key_material.update(shared_key) + key_material = key_material.finalize() + assert len(key_material) == _KEY_MATERIAL_LENGTH_BYTES + return \ + key_material[:_SYM_KEY_LENGTH_BYTES], \ + key_material[_SYM_KEY_LENGTH_BYTES:] diff --git a/zeth/errors.py b/zeth/errors.py new file mode 100644 index 0000000..a9fdb28 --- /dev/null +++ b/zeth/errors.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +# Zeth standard error messages +SNARK_NOT_SUPPORTED: str = \ + "Invalid zkSNARK, should be one of ('PGHR13', 'GROTH16')" diff --git a/zeth/merkle_tree.py b/zeth/merkle_tree.py new file mode 100644 index 0000000..4b0b9ae --- /dev/null +++ b/zeth/merkle_tree.py @@ -0,0 +1,294 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from __future__ import annotations +from zeth.mimc import MiMC7 +from os.path import exists +import json +import math +from typing import Dict, List, Tuple, Iterator, cast, Any + + +ZERO_ENTRY = bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000000") + +HASH = MiMC7() + + +class MerkleTreeData: + """ + Simple container to be persisted for a client-side Merkle tree. Does not + perform any computation. Layers are ordered from top (smallest) to bottom. + """ + def __init__( + self, + depth: int, + default_values: List[bytes], + layers: List[List[bytes]]): + self.depth = depth + self.default_values = default_values + self.layers = layers + + @staticmethod + def empty_with_depth(depth: int) -> MerkleTreeData: + # Compute default values for each layer + default_values = [ZERO_ENTRY] * (depth + 1) + for i in range(depth - 1, -1, -1): + default_values[i] = MerkleTree.combine( + default_values[i + 1], default_values[i + 1]) + + # Initial layer data (fill the 0-th layer with the default root so it's + # always available). + layers: List[List[bytes]] = [[default_values[0]]] + layers.extend([[] for _ in range(depth)]) + assert len(default_values) == depth + 1 + assert len(layers) == depth + 1 + return MerkleTreeData(depth, default_values, layers) + + @staticmethod + def from_json_dict(json_dict: Dict[str, Any]) -> MerkleTreeData: + depth = cast(int, json_dict["depth"]) + default_values = _to_list_bytes( + cast(List[str], json_dict["default_values"])) + layers = [ + _to_list_bytes(layer) + for layer in cast(List[List[str]], json_dict["layers"])] + return MerkleTreeData(depth, default_values, layers) + + def to_json_dict(self) -> Dict[str, Any]: + return { + "depth": self.depth, + "default_values": _to_list_str(self.default_values), + "layers": [_to_list_str(layer) for layer in self.layers], + } + + +class MerkleTree: + """ + Merkle tree structure matching that used in the mixer contract. Simple + implementation where unpopulated values (zeroes) are also stored. + """ + def __init__(self, tree_data: MerkleTreeData, depth: int): + self.max_num_leaves = pow(2, depth) + self.depth = tree_data.depth + self.tree_data = tree_data + self.num_new_leaves = 0 + + @staticmethod + def empty_with_depth(depth: int) -> MerkleTree: + return MerkleTree(MerkleTreeData.empty_with_depth(depth), depth) + + @staticmethod + def empty_with_size(num_leaves: int) -> MerkleTree: + depth = int(math.log(num_leaves, 2)) + assert pow(2, depth) == num_leaves, f"Non-pow-2 size {num_leaves} given" + return MerkleTree.empty_with_depth(depth) + + @staticmethod + def combine(left: bytes, right: bytes) -> bytes: + result_i = HASH.mimc_mp( + int.from_bytes(left, byteorder='big'), + int.from_bytes(right, byteorder='big')) + return result_i.to_bytes(32, byteorder='big') + + def get_num_entries(self) -> int: + return len(self.tree_data.layers[self.depth]) + + def get_leaf(self, index: int) -> bytes: + leaves = self.tree_data.layers[self.depth] + if index < len(leaves): + return leaves[index] + return ZERO_ENTRY + + def get_leaves(self) -> List[bytes]: + return self.tree_data.layers[self.depth] + + def get_node(self, layer_idx: int, node_idx: int) -> bytes: + assert layer_idx <= self.depth + assert self.num_new_leaves == 0 + layer_idx = self.depth - layer_idx + layer = self.tree_data.layers[layer_idx] + if node_idx < len(layer): + return layer[node_idx] + return self.tree_data.default_values[layer_idx] + + def get_layers(self) -> Iterator[Tuple[bytes, List[bytes]]]: + """ + Public layers iterator. + """ + assert self.num_new_leaves == 0 + return self._get_layers() + + def get_root(self) -> bytes: + assert self.num_new_leaves == 0 + return self.tree_data.layers[0][0] + + def insert(self, value: bytes) -> None: + leaves = self.tree_data.layers[self.depth] + assert len(leaves) < self.max_num_leaves + leaves.append(value) + self.num_new_leaves = self.num_new_leaves + 1 + + def recompute_root(self) -> bytes: + """ + After some new leaves have been added, perform the minimal set of hashes + to recompute the tree, expanding each layer to accommodate new nodes. + """ + if self.num_new_leaves == 0: + return self.get_root() + + layers_it = self._get_layers() + + layer_default, layer = next(layers_it) + end_idx = len(layer) + start_idx = end_idx - self.num_new_leaves + layer_size = self.max_num_leaves + + for parent_default, parent_layer in layers_it: + # Computation for each layer is performed in _recompute_layer, which + # also computes the start and end indices for the next layer in the + # tree. + start_idx, end_idx = _recompute_layer( + layer, + start_idx, + end_idx, + layer_default, + parent_layer) + layer = parent_layer + layer_default = parent_default + layer_size = int(layer_size / 2) + + self.num_new_leaves = 0 + assert len(layer) == 1 + assert layer_size == 1 + return layer[0] + + def _get_layers(self) -> Iterator[Tuple[bytes, List[bytes]]]: + """ + Internal version of layers iterator for use during updating. + With 0-th layer as the leaves (matching the public interface). + """ + default_values = self.tree_data.default_values + layers = self.tree_data.layers + for i in range(self.depth, -1, -1): + yield (default_values[i], layers[i]) + + +def compute_merkle_path(address: int, mk_tree: MerkleTree) -> List[str]: + """ + Given an "address" (index into leaves of a Merkle tree), compute the path to + the root. + """ + merkle_path: List[str] = [] + if address == -1: + return merkle_path + + # Check each bit of address in turn. If it is set, take the left node, + # otherwise take the right node. + for depth in range(mk_tree.depth): + address_bit = address & 0x1 + if address_bit: + merkle_path.append(mk_tree.get_node(depth, address - 1).hex()) + else: + merkle_path.append(mk_tree.get_node(depth, address + 1).hex()) + address = address >> 1 + return merkle_path + + +class PersistentMerkleTree(MerkleTree): + """ + Version of MerkleTree that also supports persistence. + """ + def __init__( + self, filename: str, tree_data: MerkleTreeData, depth: int): + MerkleTree.__init__(self, tree_data, depth) + self.filename = filename + + @staticmethod + def open(filename: str, max_num_leaves: int) -> PersistentMerkleTree: + depth = int(math.log(max_num_leaves, 2)) + assert max_num_leaves == int(math.pow(2, depth)) + if exists(filename): + with open(filename, "r") as tree_f: + json_dict = json.load(tree_f) + tree_data = MerkleTreeData.from_json_dict(json_dict) + assert depth == tree_data.depth + else: + tree_data = MerkleTreeData.empty_with_depth(depth) + + return PersistentMerkleTree(filename, tree_data, depth) + + def save(self) -> None: + with open(self.filename, "w") as tree_f: + json.dump(self.tree_data.to_json_dict(), tree_f) + + +def _leaf_address_to_node_address(address_leaf: int, tree_depth: int) -> int: + """ + Converts the relative address of a leaf to an absolute address in the tree + Important note: The merkle root index is 0 (not 1!) + """ + address = address_leaf + (2 ** tree_depth - 1) + if address > (2 ** (tree_depth + 1) - 1): + return -1 + return address + + +def _recompute_layer( + child_layer: List[bytes], + child_start_idx: int, + child_end_idx: int, + child_default_value: bytes, + parent_layer: List[bytes]) -> Tuple[int, int]: + """ + Recompute nodes in the parent layer that are affected by entries + [child_start_idx, child_end_idx[ in the child layer. If `child_end_idx` is + required in the calculation, the final entry of the child layer is used + (since this contains the default entry for the layer if the tree is not + full). Returns the start and end indices (within the parent layer) of + touched parent nodes. + """ + + # / \ / \ / \ + # Parent: ? ? F G H 0 + # / \ / \ / \ / \ / \ / \ + # Child: ? ? ? ? A B C D E ? ? 0 + # ^ ^ + # child_start_idx child_end_idx + + # Extend the parent layer to ensure it has enough capacity. + new_parent_layer_length = int((child_end_idx + 1) / 2) + parent_layer.extend( + [ZERO_ENTRY] * (new_parent_layer_length - len(parent_layer))) + + # Compute the further right pair to compute, and iterate left until we reach + # `child_idx_rend` (reverse-end). `child_idx_rend` is the `child_start_idx` + # rounded down to the next even number. + child_left_idx_rend = int(child_start_idx / 2) * 2 + + # If the child_end_idx is odd, the first hash must use the child layer's + # default value on the right. + if child_end_idx & 1: + child_left_idx = child_end_idx - 1 + parent_layer[child_left_idx >> 1] = MerkleTree.combine( + child_layer[child_left_idx], child_default_value) + else: + child_left_idx = child_end_idx + + # At this stage, all remaining pairs are populated. Hash pairs and write + # them to the parent layer. + while child_left_idx > child_left_idx_rend: + child_left_idx = child_left_idx - 2 + parent_layer[child_left_idx >> 1] = MerkleTree.combine( + child_layer[child_left_idx], child_layer[child_left_idx + 1]) + + return child_start_idx >> 1, new_parent_layer_length + + +def _to_list_bytes(list_str: List[str]) -> List[bytes]: + return [bytes.fromhex(entry) for entry in list_str] + + +def _to_list_str(list_bytes: List[bytes]) -> List[str]: + return [entry.hex() for entry in list_bytes] diff --git a/zeth/mimc.py b/zeth/mimc.py new file mode 100644 index 0000000..37c1f11 --- /dev/null +++ b/zeth/mimc.py @@ -0,0 +1,66 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from zeth.constants import ZETH_PRIME, MIMC_MT_SEED +from Crypto.Hash import keccak \ + # pylint: disable=import-error,no-name-in-module,line-too-long #type: ignore +from typing import Optional + + +class MiMC7: + """ + Python implementation of MiMC7 algorithm used in the mixer contract + """ + + def __init__( + self, + seed: str = MIMC_MT_SEED, + prime: int = ZETH_PRIME): + self.prime = prime + self.seed = seed + + def mimc_round(self, message: int, key: int, rc: int) -> int: + xored = (message + key + rc) % self.prime + return xored ** 7 % self.prime + + def mimc_encrypt( + self, + message: int, + ek: int, + seed: Optional[str] = None, + rounds: int = 91) -> int: + seed = seed or self.seed + res = message % self.prime + key = ek % self.prime + + # In the paper the first round constant is set to 0 + res = self.mimc_round(res, key, 0) + + round_constant: int = _keccak_256(_str_to_bytes(seed)) + + for _ in range(rounds - 1): + round_constant = _keccak_256(_int_to_bytes32(round_constant)) + res = self.mimc_round(res, key, round_constant) + + return (res + key) % self.prime + + def mimc_mp(self, x: int, y: int) -> int: + x = x % self.prime + y = y % self.prime + return (self.mimc_encrypt(x, y, self.seed) + x + y) % self.prime + + +def _str_to_bytes(value: str) -> bytes: + return value.encode('ascii') + + +def _int_to_bytes32(value: int) -> bytes: + return value.to_bytes(32, 'big') + + +def _keccak_256(data_bytes: bytes) -> int: + h = keccak.new(digest_bits=256) + h.update(data_bytes) + hashed = h.digest() + return int.from_bytes(hashed, 'big') diff --git a/zeth/mixer_client.py b/zeth/mixer_client.py new file mode 100644 index 0000000..f1f02e3 --- /dev/null +++ b/zeth/mixer_client.py @@ -0,0 +1,737 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from __future__ import annotations +import zeth.contracts as contracts +import zeth.constants as constants +from zeth.zeth_address import ZethAddressPub, ZethAddress +from zeth.ownership import OwnershipPublicKey, OwnershipSecretKey, \ + OwnershipKeyPair, ownership_key_as_hex +from zeth.encryption import \ + EncryptionPublicKey, EncryptionSecretKey, InvalidSignature, \ + generate_encryption_keypair, encrypt, decrypt +from zeth.merkle_tree import MerkleTree, compute_merkle_path +import zeth.signing as signing +from zeth.timer import Timer +from zeth.zksnark import \ + IZKSnarkProvider, get_zksnark_provider, GenericProof, GenericVerificationKey +from zeth.utils import EtherValue, get_trusted_setup_dir, \ + hex_digest_to_binary_string, digest_to_binary_string, int64_to_hex, \ + message_to_bytes, eth_address_to_bytes32, eth_uint256_to_int, to_zeth_units, \ + get_contracts_dir +from zeth.prover_client import ProverClient +from api.zeth_messages_pb2 import ZethNote, JoinsplitInput, ProofInputs + +import os +import json +from Crypto import Random +from hashlib import blake2s, sha256 +from typing import Tuple, Dict, List, Callable, Optional, Any + + +ZERO_UNITS_HEX = "0000000000000000" + +# ZethNote binary serialization format: +# [apk : APK_LENGTH_BYTES] +# [value : PUBLIC_VALUE_LENGTH_BYTES] +# [rho : RHO_LENGTH_BYTES] +# [trapr : TRAPR_LENGTH_BYTES] +_APK_OFFSET_BYTES = 0 +_VALUE_OFFSET_BYTES = _APK_OFFSET_BYTES + constants.APK_LENGTH_BYTES +_RHO_OFFSET_BYTES = _VALUE_OFFSET_BYTES + constants.PUBLIC_VALUE_LENGTH_BYTES +_TRAPR_OFFSET_BYTES = _RHO_OFFSET_BYTES + constants.RHO_LENGTH_BYTES +assert _TRAPR_OFFSET_BYTES + constants.TRAPR_LENGTH_BYTES \ + == constants.NOTE_LENGTH_BYTES + +# JoinSplit Signature Keys definitions +JoinsplitSigVerificationKey = signing.SigningVerificationKey +JoinsplitSigSecretKey = signing.SigningSecretKey +JoinsplitSigKeyPair = signing.SigningKeyPair + + +ComputeHSigCB = Callable[[bytes, bytes, JoinsplitSigVerificationKey], bytes] + + +class JoinsplitInputNote: + """ + A ZethNote, along with the nullifier and location in Merkle tree. + """ + + def __init__(self, note: ZethNote, nullifier: str, merkle_location: int): + self.note = note + self.nullifier = nullifier + self.merkle_location = merkle_location + + +def create_zeth_notes( + phi: str, + hsig: bytes, + output0: Tuple[OwnershipPublicKey, int], + output1: Tuple[OwnershipPublicKey, int] +) -> Tuple[ZethNote, ZethNote]: + """ + Create two ordered ZethNotes. This function is used to generate new output + notes. + """ + (recipient0, value0) = output0 + (recipient1, value1) = output1 + + rho0 = _compute_rho_i(phi, hsig, 0) + trap_r0 = trap_r_randomness() + note0 = ZethNote( + apk=ownership_key_as_hex(recipient0), + value=int64_to_hex(value0), + rho=rho0.hex(), + trap_r=trap_r0) + + rho1 = _compute_rho_i(phi, hsig, 1) + trap_r1 = trap_r_randomness() + note1 = ZethNote( + apk=ownership_key_as_hex(recipient1), + value=int64_to_hex(value1), + rho=rho1.hex(), + trap_r=trap_r1) + + return note0, note1 + + +def zeth_note_to_json_dict(zeth_note_grpc_obj: ZethNote) -> Dict[str, str]: + return { + "a_pk": zeth_note_grpc_obj.apk, + "value": zeth_note_grpc_obj.value, + "rho": zeth_note_grpc_obj.rho, + "trap_r": zeth_note_grpc_obj.trap_r, + } + + +def zeth_note_from_json_dict(parsed_zeth_note: Dict[str, str]) -> ZethNote: + note = ZethNote( + apk=parsed_zeth_note["a_pk"], + value=parsed_zeth_note["value"], + rho=parsed_zeth_note["rho"], + trap_r=parsed_zeth_note["trap_r"] + ) + return note + + +def zeth_note_to_bytes(zeth_note_grpc_obj: ZethNote) -> bytes: + apk_bytes = bytes.fromhex(zeth_note_grpc_obj.apk) + value_bytes = bytes.fromhex(zeth_note_grpc_obj.value) + rho_bytes = bytes.fromhex(zeth_note_grpc_obj.rho) + trap_r_bytes = bytes.fromhex(zeth_note_grpc_obj.trap_r) + note_bytes = apk_bytes + value_bytes + rho_bytes + trap_r_bytes + assert len(note_bytes) == (constants.NOTE_LENGTH_BYTES) + return note_bytes + + +def zeth_note_from_bytes(note_bytes: bytes) -> ZethNote: + if len(note_bytes) != (constants.NOTE_LENGTH_BYTES): + raise ValueError( + f"note_bytes len {len(note_bytes)}, " + f"(expected {constants.NOTE_LENGTH_BYTES})") + apk = note_bytes[ + _APK_OFFSET_BYTES:_APK_OFFSET_BYTES + constants.APK_LENGTH_BYTES] + value = note_bytes[ + _VALUE_OFFSET_BYTES: + _VALUE_OFFSET_BYTES + constants.PUBLIC_VALUE_LENGTH_BYTES] + rho = note_bytes[ + _RHO_OFFSET_BYTES:_RHO_OFFSET_BYTES + constants.RHO_LENGTH_BYTES] + trap_r = note_bytes[_TRAPR_OFFSET_BYTES:] + return ZethNote( + apk=apk.hex(), value=value.hex(), rho=rho.hex(), trap_r=trap_r.hex()) + + +def compute_commitment(zeth_note: ZethNote) -> bytes: + """ + Used by the recipient of a payment to recompute the commitment and check + the membership in the tree to confirm the validity of a payment + """ + # inner_k = blake2s(r || a_pk || rho || v) + blake = blake2s() + blake.update(bytes.fromhex(zeth_note.trap_r)) + blake.update(bytes.fromhex(zeth_note.apk)) + blake.update(bytes.fromhex(zeth_note.rho)) + blake.update(bytes.fromhex(zeth_note.value)) + cm = blake.digest() + + cm_field = int.from_bytes(cm, byteorder="big") % constants.ZETH_PRIME + return cm_field.to_bytes(int(constants.DIGEST_LENGTH/8), byteorder="big") + + +def compute_nullifier( + zeth_note: ZethNote, + spending_authority_ask: OwnershipSecretKey) -> bytes: + """ + Returns nf = blake2s(1110 || [a_sk]_252 || rho) + """ + binary_ask = digest_to_binary_string(spending_authority_ask) + first_252bits_ask = binary_ask[:252] + left_leg_bin = "1110" + first_252bits_ask + left_leg = int(left_leg_bin, 2).to_bytes(32, byteorder='big') + blake_hash = blake2s() + blake_hash.update(left_leg) + blake_hash.update(bytes.fromhex(zeth_note.rho)) + return blake_hash.digest() + + +def create_joinsplit_input( + merkle_path: List[str], + address: int, + note: ZethNote, + a_sk: OwnershipSecretKey, + nullifier: bytes) -> JoinsplitInput: + return JoinsplitInput( + merkle_path=merkle_path, + address=address, + note=note, + spending_ask=ownership_key_as_hex(a_sk), + nullifier=nullifier.hex()) + + +def write_verification_key(vk_json: GenericVerificationKey) -> None: + """ + Writes the verification key (object) in a json file + """ + setup_dir = get_trusted_setup_dir() + filename = os.path.join(setup_dir, "vk.json") + with open(filename, 'w') as outfile: + json.dump(vk_json, outfile) + + +def get_dummy_rho() -> str: + assert (constants.RHO_LENGTH_BYTES << 3) == constants.RHO_LENGTH + return bytes(Random.get_random_bytes(constants.RHO_LENGTH_BYTES)).hex() + + +def get_dummy_input_and_address( + a_pk: OwnershipPublicKey) -> Tuple[int, ZethNote]: + """ + Create a zeth note and address, for use as circuit inputs where there is no + real input. + """ + dummy_note = ZethNote( + apk=ownership_key_as_hex(a_pk), + value=ZERO_UNITS_HEX, + rho=get_dummy_rho(), + trap_r=trap_r_randomness()) + # Note that the Merkle path is not fully checked against the root by the + # circuit since the note value is 0. Hence the address used here is + # arbitrary. + dummy_note_address = 0 + return (dummy_note_address, dummy_note) + + +def compute_joinsplit2x2_inputs( + mk_root: bytes, + input0: Tuple[int, ZethNote], + mk_path0: List[str], + input1: Tuple[int, ZethNote], + mk_path1: List[str], + sender_ask: OwnershipSecretKey, + output0: Tuple[OwnershipPublicKey, int], + output1: Tuple[OwnershipPublicKey, int], + public_in_value_zeth_units: int, + public_out_value_zeth_units: int, + sign_vk: JoinsplitSigVerificationKey, + compute_h_sig_cb: Optional[ComputeHSigCB] = None +) -> ProofInputs: + """ + Create a ProofInput object for joinsplit parameters + """ + (input_address0, input_note0) = input0 + (input_address1, input_note1) = input1 + + input_nullifier0 = compute_nullifier(input_note0, sender_ask) + input_nullifier1 = compute_nullifier(input_note1, sender_ask) + js_inputs: List[JoinsplitInput] = [ + create_joinsplit_input( + mk_path0, input_address0, input_note0, sender_ask, input_nullifier0), + create_joinsplit_input( + mk_path1, input_address1, input_note1, sender_ask, input_nullifier1) + ] + + # Use the specified or default h_sig computation + compute_h_sig_cb = compute_h_sig_cb or compute_h_sig + h_sig = compute_h_sig_cb( + input_nullifier0, + input_nullifier1, + sign_vk) + phi = _phi_randomness() + + output_note0, output_note1 = create_zeth_notes( + phi, + h_sig, + output0, + output1) + + js_outputs = [ + output_note0, + output_note1 + ] + + return ProofInputs( + mk_root=mk_root.hex(), + js_inputs=js_inputs, + js_outputs=js_outputs, + pub_in_value=int64_to_hex(public_in_value_zeth_units), + pub_out_value=int64_to_hex(public_out_value_zeth_units), + h_sig=h_sig.hex(), + phi=phi) + + +class MixerClient: + """ + Interface to operations on the Mixer contract. + """ + def __init__( + self, + web3: Any, + prover_client: ProverClient, + mixer_instance: Any, + zksnark: IZKSnarkProvider): + self._prover_client = prover_client + self.web3 = web3 + self._zksnark = zksnark + self.mixer_instance = mixer_instance + + @staticmethod + def open( + web3: Any, + prover_server_endpoint: str, + mixer_instance: Any) -> MixerClient: + """ + Create a client for an existing Zeth deployment. + """ + return MixerClient( + web3, + ProverClient(prover_server_endpoint), + mixer_instance, + get_zksnark_provider(constants.ZKSNARK_DEFAULT)) + + @staticmethod + def deploy( + web3: Any, + prover_server_endpoint: str, + deployer_eth_address: str, + token_address: Optional[str] = None, + deploy_gas: Optional[EtherValue] = None, + zksnark: Optional[IZKSnarkProvider] = None) \ + -> Tuple[MixerClient, contracts.InstanceDescription]: + """ + Deploy Zeth contracts. + """ + print("[INFO] 1. Fetching verification key from the proving server") + zksnark = zksnark or get_zksnark_provider(constants.ZKSNARK_DEFAULT) + prover_client = ProverClient(prover_server_endpoint) + vk_obj = prover_client.get_verification_key() + vk_json = zksnark.parse_verification_key(vk_obj) + deploy_gas = deploy_gas or \ + EtherValue(constants.DEPLOYMENT_GAS_WEI, 'wei') + + print("[INFO] 2. Received VK, writing verification key...") + write_verification_key(vk_json) + + print("[INFO] 3. VK written, deploying smart contracts...") + contracts_dir = get_contracts_dir() + mixer_name = zksnark.get_contract_name() + mixer_src = os.path.join(contracts_dir, mixer_name + ".sol") + + verification_key_params = zksnark.verification_key_parameters(vk_json) + mixer_description = contracts.InstanceDescription.deploy( + web3, + mixer_src, + mixer_name, + deployer_eth_address, + deploy_gas, + {}, + mk_depth=constants.ZETH_MERKLE_TREE_DEPTH, + token=token_address or "0x0000000000000000000000000000000000000000", + **verification_key_params) + mixer_instance = mixer_description.instantiate(web3) + client = MixerClient(web3, prover_client, mixer_instance, zksnark) + return client, mixer_description + + def deposit( + self, + mk_tree: MerkleTree, + zeth_address: ZethAddress, + sender_eth_address: str, + eth_amount: EtherValue, + outputs: Optional[List[Tuple[ZethAddressPub, EtherValue]]] = None, + tx_value: Optional[EtherValue] = None + ) -> str: + if not outputs or len(outputs) == 0: + outputs = [(zeth_address.addr_pk, eth_amount)] + return self.joinsplit( + mk_tree, + sender_ownership_keypair=zeth_address.ownership_keypair(), + sender_eth_address=sender_eth_address, + inputs=[], + outputs=outputs, + v_in=eth_amount, + v_out=EtherValue(0), + tx_value=tx_value) + + def joinsplit( + self, + mk_tree: MerkleTree, + sender_ownership_keypair: OwnershipKeyPair, + sender_eth_address: str, + inputs: List[Tuple[int, ZethNote]], + outputs: List[Tuple[ZethAddressPub, EtherValue]], + v_in: EtherValue, + v_out: EtherValue, + tx_value: Optional[EtherValue] = None, + compute_h_sig_cb: Optional[ComputeHSigCB] = None) -> str: + mix_params = self.create_mix_parameters( + mk_tree, + sender_ownership_keypair, + sender_eth_address, + inputs, + outputs, + v_in, + v_out, + compute_h_sig_cb) + + # By default transfer exactly v_in, otherwise allow caller to manually + # specify. + tx_value = tx_value or v_in + return self.mix( + mix_params, + sender_eth_address, + tx_value.wei, + constants.DEFAULT_MIX_GAS_WEI) + + def create_mix_parameters_keep_signing_key( + self, + mk_tree: MerkleTree, + sender_ownership_keypair: OwnershipKeyPair, + sender_eth_address: str, + inputs: List[Tuple[int, ZethNote]], + outputs: List[Tuple[ZethAddressPub, EtherValue]], + v_in: EtherValue, + v_out: EtherValue, + compute_h_sig_cb: Optional[ComputeHSigCB] = None + ) -> Tuple[contracts.MixParameters, JoinsplitSigKeyPair]: + assert len(inputs) <= constants.JS_INPUTS + assert len(outputs) <= constants.JS_OUTPUTS + + sender_a_sk = sender_ownership_keypair.a_sk + sender_a_pk = sender_ownership_keypair.a_pk + inputs = \ + inputs + \ + [get_dummy_input_and_address(sender_a_pk) + for _ in range(constants.JS_INPUTS - len(inputs))] + mk_root = mk_tree.get_root() + mk_paths = [compute_merkle_path(addr, mk_tree) for addr, _ in inputs] + + # Generate output notes and proof. Dummy outputs are constructed with + # value 0 to an invalid ZethAddressPub, formed from the senders + # a_pk, and an ephemeral k_pk. + dummy_k_pk = generate_encryption_keypair().k_pk + dummy_addr_pk = ZethAddressPub(sender_a_pk, dummy_k_pk) + outputs = \ + outputs + \ + [(dummy_addr_pk, EtherValue(0)) + for _ in range(constants.JS_OUTPUTS - len(outputs))] + outputs_with_a_pk = \ + [(zeth_addr.a_pk, to_zeth_units(value)) + for (zeth_addr, value) in outputs] + + # Timer used to time proof-generation round trip time. + timer = Timer.started() + + (output_note1, output_note2, proof_json, signing_keypair) = \ + self.get_proof_joinsplit_2_by_2( + mk_root, + inputs[0], + mk_paths[0], + inputs[1], + mk_paths[1], + sender_a_sk, + outputs_with_a_pk[0], + outputs_with_a_pk[1], + to_zeth_units(v_in), + to_zeth_units(v_out), + compute_h_sig_cb) + + proof_gen_time_s = timer.elapsed_seconds() + print(f"PROOF GEN ROUND TRIP: {proof_gen_time_s} seconds") + + # Encrypt the notes + outputs_and_notes = zip(outputs, [output_note1, output_note2]) + output_notes_with_k_pk = \ + [(note, zeth_addr.k_pk) + for ((zeth_addr, _), note) in outputs_and_notes] + ciphertexts = encrypt_notes(output_notes_with_k_pk) + + # Sign + signature = joinsplit_sign( + signing_keypair, + sender_eth_address, + ciphertexts, + proof_json) + + mix_params = contracts.MixParameters( + proof_json, + signing_keypair.vk, + signature, + ciphertexts) + return mix_params, signing_keypair + + def create_mix_parameters( + self, + mk_tree: MerkleTree, + sender_ownership_keypair: OwnershipKeyPair, + sender_eth_address: str, + inputs: List[Tuple[int, ZethNote]], + outputs: List[Tuple[ZethAddressPub, EtherValue]], + v_in: EtherValue, + v_out: EtherValue, + compute_h_sig_cb: Optional[ComputeHSigCB] = None + ) -> contracts.MixParameters: + mix_params, _sig_keypair = self.create_mix_parameters_keep_signing_key( + mk_tree, + sender_ownership_keypair, + sender_eth_address, + inputs, + outputs, + v_in, + v_out, + compute_h_sig_cb) + return mix_params + + def mix( + self, + mix_params: contracts.MixParameters, + sender_eth_address: str, + wei_pub_value: int, + call_gas: int) -> str: + return contracts.mix( + self._zksnark, + self.mixer_instance, + mix_params, + sender_eth_address, + wei_pub_value, + call_gas) + + def mix_call( + self, + mix_params: contracts.MixParameters, + sender_eth_address: str, + wei_pub_value: int, + call_gas: int) -> bool: + return contracts.mix_call( + self._zksnark, + self.mixer_instance, + mix_params, + sender_eth_address, + wei_pub_value, + call_gas) + + def get_proof_joinsplit_2_by_2( + self, + mk_root: bytes, + input0: Tuple[int, ZethNote], + mk_path0: List[str], + input1: Tuple[int, ZethNote], + mk_path1: List[str], + sender_ask: OwnershipSecretKey, + output0: Tuple[OwnershipPublicKey, int], + output1: Tuple[OwnershipPublicKey, int], + public_in_value_zeth_units: int, + public_out_value_zeth_units: int, + compute_h_sig_cb: Optional[ComputeHSigCB] = None + ) -> Tuple[ZethNote, ZethNote, Dict[str, Any], JoinsplitSigKeyPair]: + """ + Query the prover server to generate a proof for the given joinsplit + parameters. + """ + signing_keypair = signing.gen_signing_keypair() + proof_input = compute_joinsplit2x2_inputs( + mk_root, + input0, + mk_path0, + input1, + mk_path1, + sender_ask, + output0, + output1, + public_in_value_zeth_units, + public_out_value_zeth_units, + signing_keypair.vk, + compute_h_sig_cb) + proof_obj = self._prover_client.get_proof(proof_input) + proof_json = self._zksnark.parse_proof(proof_obj) + + # Sanity check our unpacking code against the prover server output. + pub_inputs = proof_json["inputs"] + print(f"pub_inputs: {pub_inputs}") + # pub_inputs_bytes = [bytes.fromhex(x) for x in pub_inputs] + (v_in, v_out) = public_inputs_extract_public_values(pub_inputs) + assert public_in_value_zeth_units == v_in + assert public_out_value_zeth_units == v_out + + # We return the zeth notes to be able to spend them later + # and the proof used to create them + return ( + proof_input.js_outputs[0], # pylint: disable=no-member + proof_input.js_outputs[1], # pylint: disable=no-member + proof_json, + signing_keypair) + + +def encrypt_notes( + notes: List[Tuple[ZethNote, EncryptionPublicKey]] +) -> List[bytes]: + """ + Encrypts a set of output notes to be decrypted by the respective receivers. + Returns the ciphertexts corresponding to each note. + """ + + def _encrypt_note(out_note: ZethNote, pub_key: EncryptionPublicKey) -> bytes: + out_note_bytes = zeth_note_to_bytes(out_note) + + return encrypt(out_note_bytes, pub_key) + + ciphertexts = [_encrypt_note(note, pk) for (note, pk) in notes] + return ciphertexts + + +def receive_note( + out_ev: contracts.MixOutputEvents, + receiver_k_sk: EncryptionSecretKey +) -> Optional[Tuple[bytes, ZethNote]]: + """ + Given the receivers secret key, and the event data from a transaction + (encrypted notes), decrypt any that are intended for the receiver. Return + tuples `(, ZethNote)`. Callers should record the + address-in-merkle-tree along with ZethNote information, for convenience + when spending the notes. + """ + try: + plaintext = decrypt(out_ev.ciphertext, receiver_k_sk) + return ( + out_ev.commitment, + zeth_note_from_bytes(plaintext)) + except InvalidSignature: + return None + except ValueError: + return None + + +def _encode_proof_and_inputs(proof_json: GenericProof) -> Tuple[bytes, bytes]: + """ + Given a proof object, compute the hash of the properties excluding "inputs", + and the hash of the "inputs". + """ + + proof_elements: List[int] = [] + for key in proof_json.keys(): + if key != "inputs": + proof_elements.extend(proof_json[key]) + return ( + message_to_bytes(proof_elements), + message_to_bytes(proof_json["inputs"])) + + +def joinsplit_sign( + signing_keypair: JoinsplitSigKeyPair, + sender_eth_address: str, + ciphertexts: List[bytes], + proof_json: GenericProof, +) -> int: + """ + Generate a signature on the hash of the ciphertexts, proofs and + primary inputs. This is used to solve transaction malleability. We chose + to sign the hash and not the values themselves for modularity (to use the + same code regardless of whether GROTH16 or PGHR13 proof system is chosen), + and sign the hash of the ciphers and inputs for consistency. + """ + assert len(ciphertexts) == constants.JS_INPUTS + + # The message to sign consists of (in order): + # - senders Ethereum address + # - ciphertexts + # - proof elements + # - public input elements + h = sha256() + h.update(eth_address_to_bytes32(sender_eth_address)) + for ciphertext in ciphertexts: + h.update(ciphertext) + + proof_bytes, pub_inputs_bytes = _encode_proof_and_inputs(proof_json) + h.update(proof_bytes) + h.update(pub_inputs_bytes) + message_digest = h.digest() + return signing.sign(signing_keypair.sk, message_digest) + + +def compute_h_sig( + nf0: bytes, + nf1: bytes, + sign_vk: JoinsplitSigVerificationKey) -> bytes: + """ + Compute h_sig = sha256(nf0 || nf1 || sign_vk) + Flatten the verification key + """ + h = sha256() + h.update(nf0) + h.update(nf1) + h.update(signing.encode_vk_to_bytes(sign_vk)) + return h.digest() + + +def trap_r_randomness() -> str: + """ + Compute randomness `r` + """ + assert (constants.TRAPR_LENGTH_BYTES << 3) == constants.TRAPR_LENGTH + return bytes(Random.get_random_bytes(constants.TRAPR_LENGTH_BYTES)).hex() + + +def public_inputs_extract_public_values( + public_inputs: List[str]) -> Tuple[int, int]: + """ + Extract (v_in, v_out) from encoded public inputs. Allows client code to + check these properties of MixParameters without needing to know the details + of the structure / packing policy. + """ + residual = eth_uint256_to_int(public_inputs[constants.RESIDUAL_BITS_INDEX]) + residual = residual >> constants.TOTAL_DIGEST_RESIDUAL_BITS + v_out = (residual & constants.PUBLIC_VALUE_MASK) + v_in = \ + (residual >> constants.PUBLIC_VALUE_LENGTH) & constants.PUBLIC_VALUE_MASK + return (v_in, v_out) + + +def _compute_rho_i(phi: str, hsig: bytes, i: int) -> bytes: + """ + Returns rho_i = blake2s(0 || i || 10 || [phi]_252 || hsig) + See: Zcash protocol spec p. 57, Section 5.4.2 Pseudo Random Functions + """ + # [SANITY CHECK] make sure i is in the interval [0, JS_INPUTS]. For now, + # this code also relies on JS_INPUTS being <= 2. + assert i < constants.JS_INPUTS + assert constants.JS_INPUTS <= 2, \ + "function needs updating to support JS_INPUTS > 2" + + blake_hash = blake2s() + + # Append PRF^{rho} tag to a_sk + binary_phi = hex_digest_to_binary_string(phi) + first_252bits_phi = binary_phi[:252] + left_leg_bin = "0" + str(i) + "10" + first_252bits_phi + blake_hash.update(int(left_leg_bin, 2).to_bytes(32, byteorder='big')) + blake_hash.update(hsig) + return blake_hash.digest() + + +def _phi_randomness() -> str: + """ + Compute the transaction randomness "phi", used for computing the new rhoS + """ + return bytes(Random.get_random_bytes(constants.PHI_LENGTH_BYTES)).hex() diff --git a/zeth/ownership.py b/zeth/ownership.py new file mode 100644 index 0000000..b7c6b01 --- /dev/null +++ b/zeth/ownership.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from __future__ import annotations + +from zeth.utils import hex_extend_32bytes, digest_to_binary_string, encode_abi + +from Crypto import Random +from hashlib import blake2s +from typing import NewType + + +# Secret key for proving ownership +OwnershipSecretKey = NewType('OwnershipSecretKey', bytes) + + +# Public key for proving owenership +OwnershipPublicKey = NewType('OwnershipPublicKey', bytes) + + +class OwnershipKeyPair: + """ + Key-pair for ownership proof + """ + def __init__(self, a_sk: OwnershipSecretKey, a_pk: OwnershipPublicKey): + self.a_sk: OwnershipSecretKey = a_sk + self.a_pk: OwnershipPublicKey = a_pk + + +def ownership_key_as_hex(a_sk: bytes) -> str: + """ + Convert either a secret or public ownership key to hex representation of the + underlying 32-byte object. + """ + return hex_extend_32bytes(a_sk.hex()) + + +def ownership_public_key_from_hex(key_hex: str) -> OwnershipPublicKey: + """ + Read an ownership public key from a hex string. + """ + return OwnershipPublicKey(bytes.fromhex(key_hex)) + + +def ownership_secret_key_from_hex(key_hex: str) -> OwnershipSecretKey: + """ + Read an ownership public key from a hex string. + """ + return OwnershipSecretKey(bytes.fromhex(key_hex)) + + +def gen_ownership_keypair() -> OwnershipKeyPair: + a_sk = OwnershipSecretKey(Random.get_random_bytes(32)) + a_pk = _derive_a_pk(a_sk) + keypair = OwnershipKeyPair(a_sk, a_pk) + return keypair + + +def _derive_a_pk(a_sk: OwnershipSecretKey) -> OwnershipPublicKey: + """ + Returns a_pk = blake2s(1100 || [a_sk]_252 || 0^256) + """ + binary_a_sk = digest_to_binary_string(a_sk) + first_252bits_ask = binary_a_sk[:252] + left_leg_bin = "1100" + first_252bits_ask + left_leg_hex = "{0:0>4X}".format(int(left_leg_bin, 2)) + zeroes = "0000000000000000000000000000000000000000000000000000000000000000" + a_pk = blake2s( + encode_abi( + ["bytes32", "bytes32"], + [bytes.fromhex(left_leg_hex), bytes.fromhex(zeroes)]) + ).digest() + return OwnershipPublicKey(a_pk) diff --git a/zeth/prover_client.py b/zeth/prover_client.py new file mode 100644 index 0000000..6adbae4 --- /dev/null +++ b/zeth/prover_client.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +import grpc # type: ignore +from google.protobuf import empty_pb2 +from api.zeth_messages_pb2 import ProofInputs +from api.snark_messages_pb2 import VerificationKey, ExtendedProof +from api import prover_pb2_grpc # type: ignore + + +class ProverClient: + def __init__(self, endpoint: str): + self.endpoint = endpoint + + def get_verification_key(self) -> VerificationKey: + """ + Fetch the verification key from the proving service + """ + with grpc.insecure_channel(self.endpoint) as channel: + stub = prover_pb2_grpc.ProverStub(channel) # type: ignore + print("-------------- Get the verification key --------------") + verificationkey = stub.GetVerificationKey(_make_empty_message()) + return verificationkey + + def get_proof( + self, + proof_inputs: ProofInputs) -> ExtendedProof: + """ + Request a proof generation to the proving service + """ + with grpc.insecure_channel(self.endpoint) as channel: + stub = prover_pb2_grpc.ProverStub(channel) # type: ignore + print("-------------- Get the proof --------------") + proof = stub.Prove(proof_inputs) + return proof + + +def _make_empty_message() -> empty_pb2.Empty: + return empty_pb2.Empty() diff --git a/zeth/py.typed b/zeth/py.typed new file mode 100644 index 0000000..a0c5d77 --- /dev/null +++ b/zeth/py.typed @@ -0,0 +1,5 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +# Empty file, required for mypy. \ No newline at end of file diff --git a/zeth/signing.py b/zeth/signing.py new file mode 100644 index 0000000..174c127 --- /dev/null +++ b/zeth/signing.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +""" +Implementation of Schnorr-based one-time signature from: "Two-tier +signatures, strongly unforgeable signatures, and Fiat-Shamir without random +oracles" by Bellare and Shoup (https://eprint.iacr.org/2007/273.pdf) over Curve +BN128 +""" + +from math import ceil +from os import urandom +from hashlib import sha256 +from py_ecc import bn128 as ec +from zeth.utils import FQ, G1, g1_to_bytes +from zeth.constants import ZETH_PRIME +from typing import List + + +class SigningVerificationKey: + """ + An OT-Schnorr verification key. + """ + def __init__(self, x_g1: G1, y_g1: G1): + self.ppk = x_g1 + self.spk = y_g1 + + +class SigningSecretKey: + """ + An OT-Schnorr signing key. + """ + def __init__(self, x: FQ, y: FQ, y_g1: G1): + self.psk = x + self.ssk = (y, y_g1) + + +class SigningKeyPair: + """ + An OT-Schnorr signing and verification keypair. + """ + def __init__(self, x: FQ, y: FQ, x_g1: G1, y_g1: G1): + # We include y_g1 in the signing key + self.sk = SigningSecretKey(x, y, y_g1) + self.vk = SigningVerificationKey(x_g1, y_g1) + + +Signature = int + + +def gen_signing_keypair() -> SigningKeyPair: + """ + Return a one-time signature key-pair + composed of elements of F_q and G1. + """ + key_size_byte = ceil(len("{0:b}".format(ZETH_PRIME)) / 8) + x = FQ( + int(bytes(urandom(key_size_byte)).hex(), 16) % ZETH_PRIME) + y = FQ( + int(bytes(urandom(key_size_byte)).hex(), 16) % ZETH_PRIME) + X = ec.multiply(ec.G1, x.n) + Y = ec.multiply(ec.G1, y.n) + return SigningKeyPair(x, y, X, Y) + + +def encode_vk_to_bytes(vk: SigningVerificationKey) -> bytes: + """ + Encode a verification key as a byte string + We assume here the group prime $p$ is written in less than 256 bits + to conform with Ethereum bytes32 type + """ + vk_byte = g1_to_bytes(vk.ppk) + vk_byte += g1_to_bytes(vk.spk) + return vk_byte + + +def encode_signature_to_bytes(signature: Signature) -> bytes: + return signature.to_bytes(32, byteorder='big') + + +def decode_signature_from_bytes(sig_bytes: bytes) -> Signature: + return int.from_bytes(sig_bytes, byteorder='big') + + +def sign( + sk: SigningSecretKey, + m: bytes) -> Signature: + """ + Generate a Schnorr signature on a message m. + We assume here that the message fits in an Ethereum word (i.e. bit_len(m) + <= 256), so that it can be represented by a single bytes32 on the smart- + contract during the signature verification. + """ + + # Encode and hash the verifying key and input hashes + challenge_to_hash = g1_to_bytes(sk.ssk[1]) + m + + # Convert the hex digest into a field element + challenge = int(sha256(challenge_to_hash).hexdigest(), 16) + challenge = challenge % ZETH_PRIME + + # Compute the signature sigma + sigma = (sk.ssk[0].n + challenge * sk.psk.n) % ZETH_PRIME + + return sigma + + +def verify( + vk: SigningVerificationKey, + m: bytes, + sigma: int) -> bool: + """ + Return true if the signature sigma is valid on message m and vk. + We assume here that the message is an hexadecimal string written in + less than 256 bits to conform with Ethereum bytes32 type. + """ + # Encode and hash the verifying key and input hashes + challenge_to_hash = g1_to_bytes(vk.spk) + m + + challenge = int(sha256(challenge_to_hash).hexdigest(), 16) + challenge = challenge % ZETH_PRIME + + left_part = ec.multiply(ec.G1, FQ(sigma).n) + right_part = ec.add(vk.spk, ec.multiply(vk.ppk, FQ(challenge).n)) + + return ec.eq(left_part, right_part) + + +def verification_key_as_mix_parameter(vk: SigningVerificationKey) -> List[int]: + """ + Transform a verification key to the format required by the mix function. + """ + return [int(vk.ppk[0]), int(vk.ppk[1]), int(vk.spk[0]), int(vk.spk[1])] + + +def verification_key_from_mix_parameter( + param: List[int]) -> SigningVerificationKey: + """ + Transform mix function parameter to verification key. + """ + return SigningVerificationKey( + (FQ(param[0]), FQ(param[1])), + (FQ(param[2]), FQ(param[3]))) + + +def signature_as_mix_parameter(signature: Signature) -> int: + """ + Transform a signature to the format required by the mix function. + """ + # This function happens to be the identity but in the general case some + # transform will be required. + return signature + + +def signature_from_mix_parameter(param: int) -> Signature: + """ + Transform mix function parameters to a signature. + """ + return param diff --git a/zeth/testing_utils.py b/zeth/testing_utils.py new file mode 100644 index 0000000..8a9583a --- /dev/null +++ b/zeth/testing_utils.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from zeth.encryption import generate_encryption_secret_key,\ + encode_encryption_secret_key, get_encryption_public_key,\ + encode_encryption_public_key + +from typing import Tuple, List +from zeth.mimc import MiMC7 + + +def gen_keys_utility( + to_print: bool = False) -> Tuple[List[bytes], List[bytes], List[bytes]]: + """ + Generates private/public keys (kP, k) over Curve25519 for Alice, Bob and + Charlie + """ + + # Alice + sk_alice = generate_encryption_secret_key() + sk_alice_bytes = encode_encryption_secret_key(sk_alice) + pk_alice = get_encryption_public_key(sk_alice) + pk_alice_bytes = encode_encryption_public_key(pk_alice) + + alice_keys_bytes = [pk_alice_bytes, sk_alice_bytes] + + # Bob + sk_bob = generate_encryption_secret_key() + sk_bob_bytes = encode_encryption_secret_key(sk_bob) + pk_bob = get_encryption_public_key(sk_bob) + pk_bob_bytes = encode_encryption_public_key(pk_bob) + + bob_keys_bytes = [pk_bob_bytes, sk_bob_bytes] + + # Charlie + sk_charlie = generate_encryption_secret_key() + sk_charlie_bytes = encode_encryption_secret_key(sk_charlie) + pk_charlie = get_encryption_public_key(sk_charlie) + pk_charlie_bytes = encode_encryption_public_key(pk_charlie) + + charlie_keys_bytes = [pk_charlie_bytes, sk_charlie_bytes] + + if to_print: + print("Alice") + print(pk_alice_bytes) + print(sk_alice_bytes) + + print("Bob") + print(pk_bob_bytes) + print(sk_bob_bytes) + + print("Charlie") + print(pk_charlie_bytes) + print(sk_charlie_bytes) + + return alice_keys_bytes, bob_keys_bytes, charlie_keys_bytes + + +def mimc_encrypt_utility() -> None: + """ + Generates test vector for MiMC encrypt + """ + m = MiMC7() + msg = 3703141493535563179657531719960160174296085208671919316200479060314459804651 # noqa + ek = \ + 15683951496311901749339509118960676303290224812129752890706581988986633412003 # noqa + ct = m.mimc_encrypt(msg, ek) + print("MiMC encrypt test vector:") + print(f"msg = {msg}") + print(f"ek = {ek}") + print(f"ct = {ct}\n") + + +def mimc_mp_utility() -> None: + """ + Generates test vector for MiMC Hash + """ + m = MiMC7() + x = 3703141493535563179657531719960160174296085208671919316200479060314459804651 # noqa + y = 15683951496311901749339509118960676303290224812129752890706581988986633412003 # noqa + + digest = m.mimc_mp(x, y) + print("MiMC MP test vector:") + print(f"x = {x}") + print(f"y = {y}") + print(f"digest = {digest}\n") + + +def mimc_tree_utility() -> None: + """ + # Generates test vectors for testing the MiMC Merkle Tree contract. A + # 16 entry (4 level) merkle tree with 0 values everywhere. + """ + m = MiMC7() + level_3 = m.mimc_mp(0, 0) + level_2 = m.mimc_mp(level_3, level_3) + level_1 = m.mimc_mp(level_2, level_2) + root = m.mimc_mp(level_1, level_1) + + print("MiMC Tree test vector (4 entries, all zero):") + + print(f"Level 2 = {level_3}") + print(f"Level 2 = {level_2}") + print(f"Level 1 = {level_1}") + print(f"Root = {root}\n") diff --git a/zeth/timer.py b/zeth/timer.py new file mode 100644 index 0000000..c56eadd --- /dev/null +++ b/zeth/timer.py @@ -0,0 +1,30 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from __future__ import annotations +import time +from typing import Optional + + +class Timer: + """ + Very simple class to help measure time. + """ + + def __init__(self) -> None: + self._start_time: Optional[float] = None + + def start(self) -> None: + assert self._start_time is None + self._start_time = time.time() + + @staticmethod + def started() -> Timer: + timer = Timer() + timer.start() + return timer + + def elapsed_seconds(self) -> float: + assert self._start_time is not None + return time.time() - self._start_time diff --git a/zeth/utils.py b/zeth/utils.py new file mode 100644 index 0000000..c96078f --- /dev/null +++ b/zeth/utils.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +# Parse the arguments given to the script + +from __future__ import annotations +from . import constants +from . import errors + +import argparse +import sys +import os +from os.path import join, dirname, normpath +import eth_abi +from web3 import Web3, HTTPProvider # type: ignore +from py_ecc import bn128 as ec +from typing import List, Tuple, Union, Any, cast + +# Some Ethereum node implementations can cause a timeout if the contract +# execution takes too long. We expect the contract to complete in under 30s on +# most machines, but allow 1 min. +WEB3_HTTP_PROVIDER_TIMEOUT_SEC = 60 + + +def open_web3(url: str) -> Any: + """ + Create a Web3 context from an http URL. + """ + return Web3(HTTPProvider( + url, + request_kwargs={'timeout': WEB3_HTTP_PROVIDER_TIMEOUT_SEC})) + + +FQ = ec.FQ +G1 = Tuple[ec.FQ, ec.FQ] + + +class EtherValue: + """ + Representation of some amount of Ether (or any token) in terms of Wei. + Disambiguates Ether values from other units such as zeth_units. + """ + def __init__(self, val: Union[str, int, float], units: str = 'ether'): + self.wei = Web3.toWei(val, units) + + def __str__(self) -> str: + return str(self.wei) + + def __add__(self, other: EtherValue) -> EtherValue: + return EtherValue(self.wei + other.wei, 'wei') + + def __sub__(self, other: EtherValue) -> EtherValue: + return EtherValue(self.wei - other.wei, 'wei') + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, EtherValue): + return False + return self.wei == other.wei + + def __ne__(self, other: Any) -> bool: + return not self.__eq__(other) + + def __lt__(self, other: EtherValue) -> bool: + return self.wei < other.wei + + def __le__(self, other: EtherValue) -> bool: + return self.wei <= other.wei + + def __gt__(self, other: EtherValue) -> bool: + return self.wei > other.wei + + def __ge__(self, other: EtherValue) -> bool: + return self.wei >= other.wei + + def __bool__(self) -> bool: + return int(self.wei) != 0 + + def ether(self) -> str: + return str(Web3.fromWei(self.wei, 'ether')) + + +def encode_single(type_name: str, data: bytes) -> bytes: + """ + Typed wrapper around eth_abi.encode_single + """ + return eth_abi.encode_single(type_name, data) # type: ignore + + +def encode_abi(type_names: List[str], data: List[bytes]) -> bytes: + """ + Typed wrapper around eth_abi.encode_abi + """ + return eth_abi.encode_abi(type_names, data) # type: ignore + + +def eth_address_to_bytes(eth_addr: str) -> bytes: + """ + Binary encoding of ethereum address to 20 bytes + """ + # Strip the leading '0x' and hex-decode. + assert len(eth_addr) == 42 + assert eth_addr.startswith("0x") + return bytes.fromhex(eth_addr[2:]) + + +def eth_address_to_bytes32(eth_addr: str) -> bytes: + """ + Binary encoding of ethereum address to 32 bytes + """ + return extend_32bytes(eth_address_to_bytes(eth_addr)) + + +def eth_uint256_to_int(eth_uint256: str) -> int: + assert isinstance(eth_uint256, str) + assert eth_uint256.startswith("0x") + return int.from_bytes( + bytes.fromhex(hex_extend_32bytes(eth_uint256[2:])), + byteorder='big') + + +def g1_to_bytes(group_el: G1) -> bytes: + """ + Encode a group element into a byte string + We assume here the group prime $p$ is written in less than 256 bits + to conform with Ethereum bytes32 type. + """ + return \ + int(group_el[0]).to_bytes(32, byteorder='big') + \ + int(group_el[1]).to_bytes(32, byteorder='big') + + +def int64_to_bytes(number: int) -> bytes: + return number.to_bytes(8, 'big') + + +def int64_to_hex(number: int) -> str: + return int64_to_bytes(number).hex() + + +def hex_digest_to_binary_string(digest: str) -> str: + if len(digest) % 2 == 1: + digest = "0" + digest + return "".join(["{0:04b}".format(int(c, 16)) for c in digest]) + + +def digest_to_binary_string(digest: bytes) -> str: + return "".join(["{0:08b}".format(b) for b in digest]) + + +def hex_to_int(elements: List[str]) -> List[int]: + """ + Given an array of hex strings, return an array of int values + """ + return [int(x, 16) for x in elements] + + +def extend_32bytes(value: bytes) -> bytes: + """ + Pad value on the left with zeros, to make 32 bytes. + """ + assert len(value) <= 32 + return bytes(32-len(value)) + value + + +def hex_extend_32bytes(element: str) -> str: + """ + Extend a hex string to represent 32 bytes + """ + res = str(element) + if len(res) % 2 != 0: + res = "0" + res + return extend_32bytes(bytes.fromhex(res)).hex() + + +def to_zeth_units(value: EtherValue) -> int: + """ + Convert a quantity of ether / token to Zeth units + """ + return int(value.wei / constants.ZETH_PUBLIC_UNIT_VALUE) + + +def from_zeth_units(zeth_units: int) -> EtherValue: + """ + Convert a quantity of ether / token to Zeth units + """ + return EtherValue(zeth_units * constants.ZETH_PUBLIC_UNIT_VALUE, "wei") + + +def parse_zksnark_arg() -> str: + """ + Parse the zksnark argument and return its value + """ + parser = argparse.ArgumentParser( + description="Testing Zeth transactions using the specified zkSNARK " + + "('GROTH16' or 'PGHR13').\nNote that the zkSNARK must match the one " + + "used on the prover server.") + parser.add_argument("zksnark", help="Set the zkSNARK to use") + args = parser.parse_args() + if args.zksnark not in constants.VALID_ZKSNARKS: + return sys.exit(errors.SNARK_NOT_SUPPORTED) + return args.zksnark + + +def get_zeth_dir() -> str: + return os.environ.get( + 'ZETH', + normpath(join(dirname(__file__), "..", ".."))) + + +def get_trusted_setup_dir() -> str: + return os.environ.get( + 'ZETH_TRUSTED_SETUP_DIR', + join(get_zeth_dir(), "trusted_setup")) + + +def get_contracts_dir() -> str: + return os.environ.get( + 'ZETH_CONTRACTS_DIR', + join(get_zeth_dir(), "zeth_contracts", "contracts")) + + +def string_list_flatten( + strs_list: Union[List[str], List[Union[str, List[str]]]]) -> List[str]: + """ + Flatten a list containing strings or lists of strings. + """ + if any(isinstance(el, (list, tuple)) for el in strs_list): + strs: List[str] = [] + for el in strs_list: + if isinstance(el, (list, tuple)): + strs.extend(el) + else: + strs.append(cast(str, el)) + return strs + + return cast(List[str], strs_list) + + +def message_to_bytes(message_list: Any) -> bytes: + # message_list: Union[List[str], List[Union[int, str, List[str]]]]) -> bytes: + """ + Encode a list of variables, or list of lists of variables into a byte + vector + """ + + messages = string_list_flatten(message_list) + + data_bytes = bytearray() + for m in messages: + # For each element + m_hex = m + + # Convert it into a hex + if isinstance(m, int): + m_hex = "{0:0>4X}".format(m) + elif isinstance(m, str) and (m[1] == "x"): + m_hex = m[2:] + + # [SANITY CHECK] Make sure the hex is 32 byte long + m_hex = hex_extend_32bytes(m_hex) + + # Encode the hex into a byte array and append it to result + data_bytes += encode_single("bytes32", bytes.fromhex(m_hex)) + + return data_bytes + + +def short_commitment(cm: bytes) -> str: + """ + Summary of the commitment value, in some standard format. + """ + return cm[0:4].hex() diff --git a/zeth/wallet.py b/zeth/wallet.py new file mode 100644 index 0000000..e954f31 --- /dev/null +++ b/zeth/wallet.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from __future__ import annotations +from zeth.zeth_address import ZethAddressPriv +from zeth.mixer_client import zeth_note_to_json_dict, zeth_note_from_json_dict, \ + receive_note, compute_nullifier, compute_commitment +from zeth.constants import ZETH_MERKLE_TREE_DEPTH +from zeth.contracts import MixOutputEvents +from zeth.merkle_tree import PersistentMerkleTree +from zeth.utils import EtherValue, short_commitment, from_zeth_units +from api.zeth_messages_pb2 import ZethNote +from os.path import join, basename, exists +from os import makedirs +from shutil import move +from typing import Dict, List, Tuple, Optional, Iterator, Any, cast +import glob +import json +import math + + +# pylint: disable=too-many-instance-attributes + +SPENT_SUBDIRECTORY: str = "spent" +MERKLE_TREE_FILE: str = "merkle-tree.dat" + +# Map nullifier to short commitment string identifying the commitment. +NullifierMap = Dict[str, str] + + +class ZethNoteDescription: + """ + All secret data about a single ZethNote, including address in the merkle + tree and the commit value. + """ + def __init__(self, note: ZethNote, address: int, commitment: bytes): + self.note = note + self.address = address + self.commitment = commitment + + def as_input(self) -> Tuple[int, ZethNote]: + """ + Returns the description in a form suitable for joinsplit. + """ + return (self.address, self.note) + + def to_json(self) -> str: + json_dict = { + "note": zeth_note_to_json_dict(self.note), + "address": str(self.address), + "commitment": self.commitment.hex(), + } + return json.dumps(json_dict, indent=4) + + @staticmethod + def from_json(json_str: str) -> ZethNoteDescription: + json_dict = json.loads(json_str) + return ZethNoteDescription( + note=zeth_note_from_json_dict(json_dict["note"]), + address=int(json_dict["address"]), + commitment=bytes.fromhex(json_dict["commitment"])) + + +class WalletState: + """ + State to be saved in the wallet (excluding individual notes). As well as + the next block to query, we store some information about the state of the + Zeth deployment such as the number of notes or the number of distinct + addresses seen. This can be useful to estimate the security of a given + transaction. + """ + def __init__( + self, next_block: int, num_notes: int, nullifier_map: NullifierMap): + self.next_block = next_block + self.num_notes = num_notes + self.nullifier_map = nullifier_map + + def to_json(self) -> str: + json_dict = { + "next_block": self.next_block, + "num_notes": self.num_notes, + "nullifier_map": self.nullifier_map, + } + return json.dumps(json_dict, indent=4) + + @staticmethod + def from_json(json_str: str) -> WalletState: + json_dict = json.loads(json_str) + return WalletState( + next_block=int(json_dict["next_block"]), + num_notes=int(json_dict["num_notes"]), + nullifier_map=cast(NullifierMap, json_dict["nullifier_map"])) + + +def _load_state_or_default(state_file: str) -> WalletState: + if not exists(state_file): + return WalletState(1, 0, {}) + with open(state_file, "r") as state_f: + return WalletState.from_json(state_f.read()) + + +def _save_state(state_file: str, state: WalletState) -> None: + with open(state_file, "w") as state_f: + state_f.write(state.to_json()) + + +class Wallet: + """ + Very simple class to track the list of notes owned by a Zeth user. + + Note: this class does not store the notes in encrypted form, and encodes + some information (including value) in the filename. It is a proof of + concept implementation and NOT intended to be secure against intruders who + have access to the file system. However, we expect that a secure + implementation could expose similar interface and functionality. + """ + def __init__( + self, + mixer_instance: Any, + username: str, + wallet_dir: str, + secret_address: ZethAddressPriv): + # k_sk_receiver: EncryptionSecretKey): + assert "_" not in username + self.mixer_instance = mixer_instance + self.username = username + self.wallet_dir = wallet_dir + self.a_sk = secret_address.a_sk + self.k_sk_receiver = secret_address.k_sk + self.state_file = join(wallet_dir, f"state_{username}") + self.state = _load_state_or_default(self.state_file) + _ensure_dir(join(self.wallet_dir, SPENT_SUBDIRECTORY)) + self.merkle_tree = PersistentMerkleTree.open( + join(wallet_dir, MERKLE_TREE_FILE), + int(math.pow(2, ZETH_MERKLE_TREE_DEPTH))) + self.merkle_tree_changed = False + self.next_addr = self.merkle_tree.get_num_entries() + + def receive_note( + self, + comm_addr: int, + out_ev: MixOutputEvents) -> Optional[ZethNoteDescription]: + # Check this output event to see if it belongs to this wallet. + our_note = receive_note(out_ev, self.k_sk_receiver) + if our_note is None: + return None + + (commit, note) = our_note + if not _check_note(commit, note): + return None + + note_desc = ZethNoteDescription(note, comm_addr, commit) + self._write_note(note_desc) + + # Add the nullifier to the map in the state file + nullifier = compute_nullifier(note_desc.note, self.a_sk) + self.state.nullifier_map[nullifier.hex()] = \ + short_commitment(commit) + return note_desc + + def receive_notes( + self, + output_events: List[MixOutputEvents]) -> List[ZethNoteDescription]: + """ + Decrypt any notes we can, verify them as being valid, and store them in + the database. + """ + new_notes = [] + + self.merkle_tree_changed = len(output_events) != 0 + for out_ev in output_events: + print( + f"wallet.receive_notes: idx:{self.next_addr}, " + + f"comm:{out_ev.commitment[:8].hex()}") + + # All commitments must be added to the tree in order. + self.merkle_tree.insert(out_ev.commitment) + note_desc = self.receive_note(self.next_addr, out_ev) + if note_desc is not None: + new_notes.append(note_desc) + + self.next_addr = self.next_addr + 1 + + # Record full set of notes seen to keep an estimate of the total in the + # mixer. + self.state.num_notes = self.state.num_notes + len(output_events) + + return new_notes + + def mark_nullifiers_used(self, nullifiers: List[bytes]) -> List[str]: + """ + Process nullifiers, marking any of our notes that they spend. + """ + commits: List[str] = [] + for nullifier in nullifiers: + nullifier_hex = nullifier.hex() + short_commit = self.state.nullifier_map.get(nullifier_hex, None) + if short_commit: + commits.append(short_commit) + self._mark_note_spent(nullifier_hex, short_commit) + + return commits + + def note_summaries(self) -> Iterator[Tuple[int, str, EtherValue]]: + """ + Returns simple information that can be efficiently read from the notes + store. + """ + return self._decode_note_files_in_dir(self.wallet_dir) + + def spent_note_summaries(self) -> Iterator[Tuple[int, str, EtherValue]]: + """ + Returns simple info from note filenames in the spent directory. + """ + return self._decode_note_files_in_dir( + join(self.wallet_dir, SPENT_SUBDIRECTORY)) + + def get_next_block(self) -> int: + return self.state.next_block + + def update_and_save_state(self, next_block: int) -> None: + self.state.next_block = next_block + _save_state(self.state_file, self.state) + self._save_merkle_tree_if_changed() + + def find_note(self, note_id: str) -> ZethNoteDescription: + note_file = self._find_note_file(note_id) + if not note_file: + raise Exception(f"no note with id {note_id}") + with open(note_file, "r") as note_f: + return ZethNoteDescription.from_json(note_f.read()) + + def _save_merkle_tree_if_changed(self) -> None: + if self.merkle_tree_changed: + self.merkle_tree_changed = False + self.merkle_tree.recompute_root() + self.merkle_tree.save() + + def _write_note(self, note_desc: ZethNoteDescription) -> None: + """ + Write a note to the database (currently just a file-per-note). + """ + note_filename = join(self.wallet_dir, self._note_basename(note_desc)) + with open(note_filename, "w") as note_f: + note_f.write(note_desc.to_json()) + + def _mark_note_spent(self, nullifier_hex: str, short_commit: str) -> None: + """ + Mark a note as having been spent. Find the file, move it to the `spent` + subdirectory, and remove the entry from the `nullifier_map`. + """ + note_file = self._find_note_file(short_commit) + if note_file is None: + raise Exception(f"expected to find file for commit {short_commit}") + spent_file = \ + join(self.wallet_dir, SPENT_SUBDIRECTORY, basename(note_file)) + move(note_file, spent_file) + del self.state.nullifier_map[nullifier_hex] + + def _note_basename(self, note_desc: ZethNoteDescription) -> str: + value_eth = from_zeth_units(int(note_desc.note.value, 16)).ether() + cm_str = short_commitment(note_desc.commitment) + return "note_%s_%04d_%s_%s" % ( + self.username, note_desc.address, cm_str, value_eth) + + @staticmethod + def _decode_basename(filename: str) -> Tuple[int, str, EtherValue]: + components = filename.split("_") + addr = int(components[2]) + short_commit = components[3] + value = EtherValue(components[4], 'ether') + return (addr, short_commit, value) + + def _decode_note_files_in_dir( + self, dir_name: str) -> Iterator[Tuple[int, str, EtherValue]]: + wildcard = join(dir_name, f"note_{self.username}_*") + filenames = sorted(glob.glob(wildcard)) + for filename in filenames: + try: + yield self._decode_basename(basename(filename)) + # print(f"wallet: _decoded_note_filenames: file={filename}") + except ValueError: + # print(f"wallet: _decoded_note_filenames: FAILED {filename}") + continue + + def _find_note_file(self, key: str) -> Optional[str]: + """ + Given some (fragment of) address or short commit, try to uniquely + identify a note file. + """ + # If len <= 4, assume it's an address, otherwise a commit + if len(key) < 5: + try: + addr = "%04d" % int(key) + wildcard = f"note_{self.username}_{addr}_*" + except Exception: + return None + else: + wildcard = f"note_{self.username}_*_{key}_*" + + candidates = list(glob.glob(join(self.wallet_dir, wildcard))) + return candidates[0] if len(candidates) == 1 else None + + +def _check_note(commit: bytes, note: ZethNote) -> bool: + """ + Recalculate the note commitment and check that it matches `commit`, the + value emitted by the contract. + """ + cm = compute_commitment(note) + if commit != cm: + print(f"WARN: bad commitment commit={commit.hex()}, cm={cm.hex()}") + return False + return True + + +def _ensure_dir(directory_name: str) -> None: + if not exists(directory_name): + makedirs(directory_name) diff --git a/zeth/zeth_address.py b/zeth/zeth_address.py new file mode 100644 index 0000000..6a2d27a --- /dev/null +++ b/zeth/zeth_address.py @@ -0,0 +1,113 @@ +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +from __future__ import annotations +from zeth.ownership import OwnershipPublicKey, OwnershipSecretKey, \ + OwnershipKeyPair, ownership_key_as_hex, gen_ownership_keypair, \ + ownership_public_key_from_hex, ownership_secret_key_from_hex +from zeth.encryption import \ + EncryptionKeyPair, EncryptionPublicKey, EncryptionSecretKey, \ + generate_encryption_keypair, encryption_public_key_as_hex, \ + encryption_public_key_from_hex, encryption_secret_key_as_hex, \ + encryption_secret_key_from_hex +import json +from typing import Dict, Any + + +class ZethAddressPub: + """ + Public half of a zethAddress. addr_pk = (a_pk and k_pk) + """ + def __init__(self, a_pk: OwnershipPublicKey, k_pk: EncryptionPublicKey): + self.a_pk: OwnershipPublicKey = a_pk + self.k_pk: EncryptionPublicKey = k_pk + + def __str__(self) -> str: + """ + Write the address as ":". + (Technically the ":" is not required, since the first key is written + with fixed length, but a separator provides some limited sanity + checking). + """ + a_pk_hex = ownership_key_as_hex(self.a_pk) + k_pk_hex = encryption_public_key_as_hex(self.k_pk) + return f"{a_pk_hex}:{k_pk_hex}" + + @staticmethod + def parse(key_hex: str) -> ZethAddressPub: + owner_enc = key_hex.split(":") + if len(owner_enc) != 2: + raise Exception("invalid JoinSplitPublicKey format") + a_pk = ownership_public_key_from_hex(owner_enc[0]) + k_pk = encryption_public_key_from_hex(owner_enc[1]) + return ZethAddressPub(a_pk, k_pk) + + +class ZethAddressPriv: + """ + Secret half of a zethAddress. addr_sk = (a_sk and k_sk) + """ + def __init__(self, a_sk: OwnershipSecretKey, k_sk: EncryptionSecretKey): + self.a_sk: OwnershipSecretKey = a_sk + self.k_sk: EncryptionSecretKey = k_sk + + def to_json(self) -> str: + return json.dumps(self._to_json_dict()) + + @staticmethod + def from_json(key_json: str) -> ZethAddressPriv: + return ZethAddressPriv._from_json_dict(json.loads(key_json)) + + def _to_json_dict(self) -> Dict[str, Any]: + return { + "a_sk": ownership_key_as_hex(self.a_sk), + "k_sk": encryption_secret_key_as_hex(self.k_sk), + } + + @staticmethod + def _from_json_dict(key_dict: Dict[str, Any]) -> ZethAddressPriv: + return ZethAddressPriv( + ownership_secret_key_from_hex(key_dict["a_sk"]), + encryption_secret_key_from_hex(key_dict["k_sk"])) + + +class ZethAddress: + """ + Secret and public keys for both ownership and encryption (referrred to as + "zethAddress" in the paper). + """ + def __init__( + self, + a_pk: OwnershipPublicKey, + k_pk: EncryptionPublicKey, + a_sk: OwnershipSecretKey, + k_sk: EncryptionSecretKey): + self.addr_pk = ZethAddressPub(a_pk, k_pk) + self.addr_sk = ZethAddressPriv(a_sk, k_sk) + + @staticmethod + def from_key_pairs( + ownership: OwnershipKeyPair, + encryption: EncryptionKeyPair) -> ZethAddress: + return ZethAddress( + ownership.a_pk, + encryption.k_pk, + ownership.a_sk, + encryption.k_sk) + + @staticmethod + def from_secret_public( + js_secret: ZethAddressPriv, + js_public: ZethAddressPub) -> ZethAddress: + return ZethAddress( + js_public.a_pk, js_public.k_pk, js_secret.a_sk, js_secret.k_sk) + + def ownership_keypair(self) -> OwnershipKeyPair: + return OwnershipKeyPair(self.addr_sk.a_sk, self.addr_pk.a_pk) + + +def generate_zeth_address() -> ZethAddress: + ownership_keypair = gen_ownership_keypair() + encryption_keypair = generate_encryption_keypair() + return ZethAddress.from_key_pairs(ownership_keypair, encryption_keypair) diff --git a/zeth/zksnark.py b/zeth/zksnark.py new file mode 100644 index 0000000..f4a6bff --- /dev/null +++ b/zeth/zksnark.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2015-2020 Clearmatics Technologies Ltd +# +# SPDX-License-Identifier: LGPL-3.0+ + +""" +zk-SNARK abstraction +""" + +from zeth.utils import hex_to_int +import zeth.constants as constants +from api.snark_messages_pb2 import VerificationKey, ExtendedProof +from api.ec_group_messages_pb2 import HexPointBaseGroup1Affine, \ + HexPointBaseGroup2Affine + +import json +from abc import (ABC, abstractmethod) +from typing import Dict, List, Tuple, Any +# pylint: disable=unnecessary-pass + +# Dictionary representing a VerificationKey from any supported snark +GenericVerificationKey = Dict[str, Any] + +# Dictionary representing a Proof from any supported snark +GenericProof = Dict[str, Any] + + +class IZKSnarkProvider(ABC): + """ + Interface to be implemented by specific zk-snark providers. Ideally, the + rest of the logic should deal only with this interface and have no + understanding of the underlying mechanisms. + """ + + @staticmethod + @abstractmethod + def get_contract_name() -> str: + """ + Get the verifier and mixer contracts for this SNARK. + """ + pass + + @staticmethod + @abstractmethod + def verification_key_parameters( + vk: GenericVerificationKey) -> Dict[str, List[int]]: + pass + + @staticmethod + @abstractmethod + def parse_verification_key( + vk_obj: VerificationKey) -> GenericVerificationKey: + pass + + @staticmethod + @abstractmethod + def parse_proof(proof_obj: ExtendedProof) -> GenericProof: + pass + + @staticmethod + @abstractmethod + def mixer_proof_parameters(parsed_proof: GenericProof) -> List[List[int]]: + """ + Generate the leading parameters to the mix function for this SNARK, from a + GenericProof object. + """ + pass + + +class Groth16SnarkProvider(IZKSnarkProvider): + + @staticmethod + def get_contract_name() -> str: + return constants.GROTH16_MIXER_CONTRACT + + @staticmethod + def verification_key_parameters( + vk: GenericVerificationKey) -> Dict[str, List[int]]: + return { + "Alpha": hex_to_int(vk["alpha_g1"]), + "Beta1": hex_to_int(vk["beta_g2"][0]), + "Beta2": hex_to_int(vk["beta_g2"][1]), + "Delta1": hex_to_int(vk["delta_g2"][0]), + "Delta2": hex_to_int(vk["delta_g2"][1]), + "ABC_coords": hex_to_int(sum(vk["abc_g1"], [])), + } + + @staticmethod + def parse_verification_key( + vk_obj: VerificationKey) -> GenericVerificationKey: + vk = vk_obj.groth16_verification_key + return { + "alpha_g1": _parse_hex_point_base_group1_affine(vk.alpha_g1), + "beta_g2": _parse_hex_point_base_group2_affine(vk.beta_g2), + "delta_g2": _parse_hex_point_base_group2_affine(vk.delta_g2), + "abc_g1": json.loads(vk.abc_g1), + } + + @staticmethod + def parse_proof(proof_obj: ExtendedProof) -> GenericProof: + proof = proof_obj.groth16_extended_proof + return { + "a": _parse_hex_point_base_group1_affine(proof.a), + "b": _parse_hex_point_base_group2_affine(proof.b), + "c": _parse_hex_point_base_group1_affine(proof.c), + "inputs": json.loads(proof.inputs), + } + + @staticmethod + def mixer_proof_parameters(parsed_proof: GenericProof) -> List[List[Any]]: + return [ + hex_to_int(parsed_proof["a"]), + hex_to_int(parsed_proof["b"][0] + parsed_proof["b"][1]), + hex_to_int(parsed_proof["c"])] + + +class PGHR13SnarkProvider(IZKSnarkProvider): + + @staticmethod + def get_contract_name() -> str: + return constants.PGHR13_MIXER_CONTRACT + + @staticmethod + def verification_key_parameters( + vk: GenericVerificationKey) -> Dict[str, List[int]]: + return { + "A1": hex_to_int(vk["a"][0]), + "A2": hex_to_int(vk["a"][1]), + "B": hex_to_int(vk["b"]), + "C1": hex_to_int(vk["c"][0]), + "C2": hex_to_int(vk["c"][1]), + "gamma1": hex_to_int(vk["g"][0]), + "gamma2": hex_to_int(vk["g"][1]), + "gammaBeta1": hex_to_int(vk["gb1"]), + "gammaBeta2_1": hex_to_int(vk["gb2"][0]), + "gammaBeta2_2": hex_to_int(vk["gb2"][1]), + "Z1": hex_to_int(vk["z"][0]), + "Z2": hex_to_int(vk["z"][1]), + "IC_coefficients": hex_to_int(sum(vk["IC"], [])), + } + + @staticmethod + def parse_verification_key(vk_obj: VerificationKey) -> GenericVerificationKey: + vk = vk_obj.pghr13_verification_key + return { + "a": _parse_hex_point_base_group2_affine(vk.a), + "b": _parse_hex_point_base_group1_affine(vk.b), + "c": _parse_hex_point_base_group2_affine(vk.c), + "g": _parse_hex_point_base_group2_affine(vk.gamma), + "gb1": _parse_hex_point_base_group1_affine(vk.gamma_beta_g1), + "gb2": _parse_hex_point_base_group2_affine(vk.gamma_beta_g2), + "z": _parse_hex_point_base_group2_affine(vk.z), + "IC": json.loads(vk.ic), + } + + @staticmethod + def parse_proof(proof_obj: ExtendedProof) -> GenericProof: + proof = proof_obj.pghr13_extended_proof + return { + "a": _parse_hex_point_base_group1_affine(proof.a), + "a_p": _parse_hex_point_base_group1_affine(proof.a_p), + "b": _parse_hex_point_base_group2_affine(proof.b), + "b_p": _parse_hex_point_base_group1_affine(proof.b_p), + "c": _parse_hex_point_base_group1_affine(proof.c), + "c_p": _parse_hex_point_base_group1_affine(proof.c_p), + "h": _parse_hex_point_base_group1_affine(proof.h), + "k": _parse_hex_point_base_group1_affine(proof.k), + "inputs": json.loads(proof.inputs), + } + + @staticmethod + def mixer_proof_parameters(parsed_proof: GenericProof) -> List[List[Any]]: + return [ + hex_to_int(parsed_proof["a"]) + + hex_to_int(parsed_proof["a_p"]), + [hex_to_int(parsed_proof["b"][0]), + hex_to_int(parsed_proof["b"][1])], + hex_to_int(parsed_proof["b_p"]), + hex_to_int(parsed_proof["c"]), + hex_to_int(parsed_proof["c_p"]), + hex_to_int(parsed_proof["h"]), + hex_to_int(parsed_proof["k"])] + + +def get_zksnark_provider(zksnark_name: str) -> IZKSnarkProvider: + if zksnark_name == constants.PGHR13_ZKSNARK: + return PGHR13SnarkProvider() + if zksnark_name == constants.GROTH16_ZKSNARK: + return Groth16SnarkProvider() + raise Exception(f"unknown zk-SNARK name: {zksnark_name}") + + +def _parse_hex_point_base_group1_affine( + point: HexPointBaseGroup1Affine) -> Tuple[str, str]: + return (point.x_coord, point.y_coord) + + +def _parse_hex_point_base_group2_affine( + point: HexPointBaseGroup2Affine +) -> Tuple[Tuple[str, str], Tuple[str, str]]: + return ( + (point.x_c1_coord, point.x_c0_coord), + (point.y_c1_coord, point.y_c0_coord))