def mix(ctx: Context, vin: str, vout: str, input_notes: List[str], output_specs: List[str], eth_addr: Optional[str], eth_private_key: Optional[str], wait: bool, show_parameters: 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) eth_private_key_data = load_eth_private_key(eth_private_key) # If instance uses an ERC20 token, tx_value can be 0. Otherwise it should # match vin_pub. tx_value = EtherValue(0) if mixer_desc.token else vin_pub mix_params = zeth_client.create_mix_parameters( wallet.merkle_tree, zeth_address.ownership_keypair(), eth_address, inputs, outputs, vin_pub, vout_pub) if show_parameters: print(f"mix_params={mix_params.to_json()}") tx_hash = zeth_client.mix(mix_params=mix_params, sender_eth_address=eth_address, sender_eth_private_key=eth_private_key_data, tx_value=tx_value) print(tx_hash) if wait: do_sync(zeth_client.web3, wallet, tx_hash, zeth_note_short_print)
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, None, [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 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 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, None, 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, None, [input1], [output0, output1], EtherValue(0), EtherValue(0), EtherValue(1, 'wei')) return wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash)
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)
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 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}")
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 deposit( ctx: Context, value: str, eth_addr: Optional[str], eth_private_key: Optional[str], wait: bool, show_parameters: bool) -> None: """ Deposit function """ value_pub = EtherValue(value) client_ctx = ctx.obj prover_client = create_prover_client(client_ctx) zklay_client, zklay_desc = create_zklay_client_and_zklay_desc( client_ctx, prover_client) zklay_address = load_zklay_address(client_ctx) wallet = open_zklay_wallet( zklay_client.mixer_instance, zklay_address.addr_sk, client_ctx) eth_address = load_eth_address(eth_addr) eth_private_key_data = load_eth_private_key(eth_private_key) # If instance uses an ERC20 token, tx_value can be 0. Otherwise it should # match vin_pub. tx_value = EtherValue(0) if zklay_desc.token else value_pub # Create the MixParameters object manually so they can be displayed. deposit_params = zklay_client.create_deposit(prover_client, eth_address, eth_private_key_data, zklay_address, value_pub) if show_parameters: print(f"deposit_params={deposit_params.to_json()}") tx_hash = zklay_client.zklay_deposit( deposit_params=deposit_params, sender_eth_address=eth_address, sender_eth_private_key=eth_private_key_data, tx_value=tx_value) print(tx_hash) if wait: pp = prover_client.get_configuration().pairing_parameters
def _from_json_dict(zksnark: IZKSnarkProvider, json_dict: Dict[str, Any]) -> DepositParameters: ext_proof = ExtendedProof.from_json_dict(zksnark, json_dict["extended_proof"]) zklay_address = decode_encryption_public_key( json_dict["zklay_address"]) eth_amount = EtherValue(json_dict["eth_amount"]) ciphertexts = [bytes.fromhex(x) for x in json_dict["ciphertexts"]] return DepositParameters(ext_proof, zklay_address, eth_amount, ciphertexts)
def eth_get_balance(ctx: Any, eth_addr: Optional[str], wei: bool) -> None: """ Command to get the balance of specific addresses. Support multiple queries per invocation (outputs one per line), for efficiency. """ eth_addr = load_eth_address(eth_addr) web3 = open_web3_from_network(get_eth_network(ctx.obj["eth_network"])) balance_wei = web3.eth.getBalance(eth_addr) # pylint: disable=no-member if wei: print(balance_wei) else: print(EtherValue(balance_wei, "wei").ether())
def deploy_test_token(eth_network: Optional[str], eth_addr: Optional[str], eth_private_key: Optional[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_addr = load_eth_address(eth_addr) eth_private_key_data = load_eth_private_key(eth_private_key) recipient_address = load_eth_address(recipient_address) web3 = open_web3_from_network(get_eth_network(eth_network)) token_instance = deploy_token( web3, eth_addr, eth_private_key_data, 4000000) \ # pylint: disable=no-member mint_tx_hash = mint_token(web3, token_instance, recipient_address, eth_addr, eth_private_key_data, EtherValue(mint_amount, 'ether')) web3.eth.waitForTransactionReceipt(mint_tx_hash) print(token_instance.address)
def deposit(self, mk_tree: MerkleTree, zeth_address: ZethAddress, sender_eth_address: str, sender_eth_private_key: Optional[bytes], 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, sender_eth_private_key=sender_eth_private_key, inputs=[], outputs=outputs, v_in=eth_amount, v_out=EtherValue(0), tx_value=tx_value)
def parse_output(output_str: str) -> Tuple[ZethAddressPub, EtherValue]: """ Parse a string of the form "<receiver_pub_address>,<value>" to an output specification. <receiver_pub_address> can be a file name containing the address. "<value>" is interpretted as the <default-address-file>,<value>. """ parts = output_str.split(",") if len(parts) == 1: addr = ZETH_PUBLIC_ADDRESS_FILE_DEFAULT value = parts[0] elif len(parts) == 2: addr = parts[0] value = parts[1] else: raise ClickException(f"invalid output spec: {output_str}") if exists(addr): with open(addr, "r") as addr_f: addr = addr_f.read() return (ZethAddressPub.parse(addr), EtherValue(value))
def __init__(self, mk_tree: MerkleTree, sender_ownership_keypair: OwnershipKeyPair, inputs: List[Tuple[int, api.ZethNote]], outputs: List[Tuple[ZethAddressPub, EtherValue]], v_in: EtherValue, v_out: EtherValue, compute_h_sig_cb: Optional[ComputeHSigCB] = None): assert len(inputs) <= constants.JS_INPUTS assert len(outputs) <= constants.JS_OUTPUTS self.mk_tree = mk_tree self.sender_ownership_keypair = sender_ownership_keypair self.v_in = v_in self.v_out = v_out self.compute_h_sig_cb = compute_h_sig_cb # Perform some cleaning and minimal pre-processing of the data. Compute # and store data that is not derivable from the ProverInput or Proof # structs (such as the encryption keys for receivers), making it # available to MixerClient calls. # Expand inputs with dummy entries and compute merkle paths. sender_a_pk = sender_ownership_keypair.a_pk self.inputs = \ inputs + \ [get_dummy_input_and_address(sender_a_pk) for _ in range(constants.JS_INPUTS - len(inputs))] # Pad the list of outputs if necessary if len(outputs) < constants.JS_OUTPUTS: dummy_k_pk = generate_encryption_keypair().k_pk dummy_addr_pk = ZethAddressPub(sender_a_pk, dummy_k_pk) self.outputs = \ outputs + \ [(dummy_addr_pk, EtherValue(0)) for _ in range(constants.JS_OUTPUTS - len(outputs))] else: self.outputs = outputs
def main() -> None: zksnark_name = zeth.core.utils.parse_zksnark_arg() zksnark = zeth.core.zksnark.get_zksnark_provider(zksnark_name) 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(web3, deployer_eth_address, None, 4000000) # ProverClient prover_client = ProverClient(mock.TEST_PROVER_SERVER_ENDPOINT) prover_config = prover_client.get_configuration() pp = prover_config.pairing_parameters assert prover_client.get_configuration().zksnark_name == zksnark_name # Deploy Zeth contracts tree_depth = constants.ZETH_MERKLE_TREE_DEPTH zeth_client, _contract_desc = MixerClient.deploy(web3, prover_client, deployer_eth_address, None, token_instance.address, None) tree_hash = get_tree_hash_for_pairing(pp.name) mk_tree = zeth.core.merkle_tree.MerkleTree.empty_with_depth( tree_depth, tree_hash) 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, tree_hash) 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, pp), 'bob': bob_wallet.receive_notes(out_ev, pp), 'charlie': charlie_wallet.receive_notes(out_ev, pp), } 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(web3, token_instance, bob_eth_address, deployer_eth_address, None, EtherValue(2 * scenario.BOB_DEPOSIT_ETH, 'ether')) 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, prover_client, mk_tree, bob_eth_address, keystore, zeth.core.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, prover_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, prover_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, prover_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, prover_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, prover_client, zksnark, 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, prover_client, zksnark, 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")
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 " + "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, None, EtherValue(BOB_DEPOSIT_ETH)) result_corrupt1 = \ wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) except Exception as e: print( "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, None, EtherValue(BOB_DEPOSIT_ETH)) result_corrupt2 = \ wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) except Exception as e: print( "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, None, EtherValue(BOB_DEPOSIT_ETH), 4000000) result_corrupt3 = \ wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) except Exception as e: print( "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, None, EtherValue(BOB_DEPOSIT_ETH)) 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 None, EtherValue(1, 'wei')) return wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash)
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, extproof, 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, extproof) mix_params = contracts.MixParameters(extproof, signing_keypair.vk, signature, ciphertexts) return mix_params, signing_keypair
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 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 charlie_double_withdraw( zeth_client: MixerClient, prover_client: ProverClient, zksnark: IZKSnarkProvider, mk_tree: MerkleTree, input1: Tuple[int, ZethNote], charlie_eth_address: str, keystore: mock.KeyStore) -> MixResult: """ Charlie tries to carry out a double spending by modifying the value of the nullifier of the previous payment """ pp = zeth_client.prover_config.pairing_parameters scalar_field_mod = pp.scalar_field_mod() scalar_field_capacity = pp.scalar_field_capacity print( f" === Charlie attempts to withdraw {CHARLIE_WITHDRAW_ETH}ETH once " + "more (double spend) one of his note on the Mixer ===") charlie_addr = keystore["Charlie"] charlie_apk = charlie_addr.addr_pk.a_pk # Create the an additional dummy input for the MixerClient input2 = get_dummy_input_and_address(charlie_apk) note1_value = 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( nfs: List[bytes], sign_vk: JoinsplitSigVerificationKey) -> bytes: # We disassemble the nfs to get the formatting of the primary inputs assert len(nfs) == 2 nf0 = nfs[0] nf1 = nfs[1] input_nullifier0 = nf0.hex() input_nullifier1 = nf1.hex() nf0_rev = "{0:0256b}".format(int(input_nullifier0, 16)) primary_input3_bits = nf0_rev[:scalar_field_capacity] primary_input3_res_bits = nf0_rev[scalar_field_capacity:] nf1_rev = "{0:0256b}".format(int(input_nullifier1, 16)) primary_input4_bits = nf1_rev[:scalar_field_capacity] primary_input4_res_bits = nf1_rev[scalar_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) + scalar_field_mod attack_primary_input4 = int(primary_input4_bits, 2) + scalar_field_mod # 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) - scalar_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) - scalar_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, public_data, signing_keypair = \ get_mix_parameters_components( zeth_client, prover_client, mk_tree, keystore["Charlie"].ownership_keypair(), # sender [input1, input2], [(charlie_addr.addr_pk, note1_value), (charlie_addr.addr_pk, EtherValue(0))], EtherValue(0), 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 = ", proof) print("public_data[3] = ", public_data[3]) print("public_data[4] = ", public_data[4]) public_data[3] = attack_primary_input3 public_data[4] = 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( zksnark, pp, signing_keypair, charlie_eth_address, ciphertexts, proof, public_data) mix_params = MixParameters( proof, public_data, 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 None, EtherValue(1, 'wei')) return wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash)
def mix( ctx: Context, vin: str, vout: str, input_notes: List[str], output_specs: List[str], eth_addr: Optional[str], eth_private_key: Optional[str], wait: bool, for_dispatch_call: bool, dump_parameters: Optional[str], dump_signing_keypair: Optional[str], dry_run: 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})") vin_pub = EtherValue(vin) vout_pub = EtherValue(vout) client_ctx = ctx.obj prover_client = create_prover_client(client_ctx) zeth_client, mixer_desc = create_mixer_client_and_mixer_desc( client_ctx, prover_client) 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. Otherwise it should # match vin_pub. tx_value = EtherValue(0) if mixer_desc.token else vin_pub # Create the MixParameters object manually so they can be displayed. # TODO: support saving the generated MixParameters to be sent later. mix_params, signing_keypair = \ zeth_client.create_mix_parameters_and_signing_key( prover_client, wallet.merkle_tree, zeth_address.ownership_keypair(), eth_address, inputs, outputs, vin_pub, vout_pub, for_dispatch_call=for_dispatch_call) # Dump parameters if requested if dump_parameters: if dump_parameters == '-': print(f"mix_params={mix_params.to_json()}") else: with open(dump_parameters, "w") as mix_params_f: json.dump(mix_params.to_json_dict(), mix_params_f) # Dump one-time signature keypair if requested if dump_signing_keypair: if dump_signing_keypair == '-': print(f"signing_key={signing_keypair.to_json_dict()}") else: with open(dump_signing_keypair, "w") as signing_keypair_f: json.dump(signing_keypair.to_json_dict(), signing_keypair_f) # Early-out if dry_run flag is set if for_dispatch_call or dry_run: return eth_private_key_data = load_eth_private_key(eth_private_key) tx_hash = zeth_client.mix( mix_params=mix_params, sender_eth_address=eth_address, sender_eth_private_key=eth_private_key_data, tx_value=tx_value) print(tx_hash) if wait: pp = prover_client.get_configuration().pairing_parameters do_sync(zeth_client.web3, wallet, pp, tx_hash, zeth_note_short_print)