def deploy( web3: Any, prover_client: ProverClient, deployer_eth_address: str, deployer_eth_private_key: Optional[bytes], token_address: Optional[str] = None, permitted_dispatcher: Optional[str] = None, vk_hash: Optional[str] = None, deploy_gas: Optional[int] = None ) -> Tuple[MixerClient, contracts.InstanceDescription]: """ Deploy Zeth contracts. """ prover_config = prover_client.get_configuration() vk = prover_client.get_verification_key() contracts_dir = get_contracts_dir() zksnark = get_zksnark_provider(prover_config.zksnark_name) pp = prover_config.pairing_parameters mixer_name = zksnark.get_contract_name(pp) mixer_src = os.path.join(contracts_dir, mixer_name + ".sol") vk_hash_evm = list(hex_to_uint256_list(vk_hash)) if vk_hash else [0, 0] assert len(vk_hash_evm) == 2 # Constructor parameters have the form: # uint256 mk_depth # address token # ... snark-specific key data ... constructor_parameters: List[Any] = [ constants.ZETH_MERKLE_TREE_DEPTH, # mk_depth token_address or ZERO_ADDRESS, # token zksnark.verification_key_to_contract_parameters(vk, pp), # vk permitted_dispatcher or ZERO_ADDRESS, # permitted_dispatcher vk_hash_evm # vk_hash ] mixer_description = contracts.InstanceDescription.deploy( web3, mixer_src, mixer_name, deployer_eth_address, deployer_eth_private_key, deploy_gas, compiler_flags={}, args=constructor_parameters) mixer_instance = mixer_description.instantiate(web3) client = MixerClient(web3, prover_config, mixer_instance) return client, mixer_description
def deploy( web3: Any, prover_client: ProverClient, deployer_eth_address: str, deployer_eth_private_key: Optional[bytes], token_address: Optional[str] = None, deploy_gas: Optional[int] = None ) -> Tuple[MixerClient, contracts.InstanceDescription]: """ Deploy Zeth contracts. """ prover_config = prover_client.get_configuration() zksnark = get_zksnark_provider(prover_config.zksnark_name) vk_proto = prover_client.get_verification_key() pp = prover_config.pairing_parameters vk = zksnark.verification_key_from_proto(vk_proto) deploy_gas = deploy_gas or constants.DEPLOYMENT_GAS_WEI contracts_dir = get_contracts_dir() mixer_name = zksnark.get_contract_name(pp) mixer_src = os.path.join(contracts_dir, mixer_name + ".sol") # Constructor parameters have the form: # uint256 mk_depth # address token # ... snark-specific key data ... constructor_parameters: List[Any] = [ constants.ZETH_MERKLE_TREE_DEPTH, # mk_depth token_address or ZERO_ADDRESS, # token zksnark.verification_key_to_contract_parameters(vk, pp), # vk ] mixer_description = contracts.InstanceDescription.deploy( web3, mixer_src, mixer_name, deployer_eth_address, deployer_eth_private_key, deploy_gas, compiler_flags={}, args=constructor_parameters) mixer_instance = mixer_description.instantiate(web3) client = MixerClient(web3, prover_config, mixer_instance) return client, mixer_description
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() # 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] # 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 = zeth.core.constants.ZETH_MERKLE_TREE_DEPTH zeth_client, _contract_desc = MixerClient.deploy( web3, prover_client, deployer_eth_address, None, None, None) # 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. 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 + "-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, 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: 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, prover_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, prover_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, prover_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, prover_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, prover_client, zksnark, 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, 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) 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")
def charlie_corrupt_bob_deposit( zeth_client: MixerClient, prover_client: ProverClient, zksnark: IZKSnarkProvider, mk_tree: MerkleTree, bob_eth_address: str, charlie_eth_address: str, keystore: mock.KeyStore) -> 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_addr_pk = keystore["Bob"] bob_apk = bob_addr_pk.addr_pk.a_pk # Get pairing parameters pp = prover_client.get_configuration().pairing_parameters # Create the JoinSplit dummy inputs for the deposit input1 = get_dummy_input_and_address(bob_apk) input2 = get_dummy_input_and_address(bob_apk) note1_value = EtherValue(BOB_SPLIT_1_ETH) note2_value = EtherValue(BOB_SPLIT_2_ETH) v_in = EtherValue(BOB_DEPOSIT_ETH) output_note1, output_note2, proof, public_data, joinsplit_keypair = \ get_mix_parameters_components( zeth_client, prover_client, mk_tree, keystore["Bob"].ownership_keypair(), [input1, input2], [(bob_addr_pk.addr_pk, note1_value), (bob_addr_pk.addr_pk, note2_value)], v_in, 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( zksnark, pp, joinsplit_keypair, charlie_eth_address, ciphertexts, proof, public_data) mix_params = MixParameters( proof, public_data, 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( zksnark, pp, new_joinsplit_keypair, charlie_eth_address, [fake_ciphertext0, fake_ciphertext1], proof, public_data) mix_params = MixParameters( proof, public_data, 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( zksnark, pp, joinsplit_keypair, bob_eth_address, ciphertexts, proof, public_data) mix_params = MixParameters( proof, public_data, 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( zksnark, pp, joinsplit_keypair, bob_eth_address, ciphertexts, proof, public_data) mix_params = MixParameters( proof, public_data, 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 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")