Exemplo n.º 1
0
    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
Exemplo n.º 2
0
    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
Exemplo n.º 3
0
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")
Exemplo n.º 4
0
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)
Exemplo n.º 5
0
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")