From 0364dba5cafd6eadeb326c5a505807ad5d69e829 Mon Sep 17 00:00:00 2001 From: Sergi Delgado Segura Date: Tue, 31 Mar 2020 16:56:52 +0200 Subject: [PATCH] Fixes e2e tests to match the user authentication apporach Last two tests are patched, user data must be stored between restarts --- test/teos/e2e/test_basic_e2e.py | 389 ++++++++++++++++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 test/teos/e2e/test_basic_e2e.py diff --git a/test/teos/e2e/test_basic_e2e.py b/test/teos/e2e/test_basic_e2e.py new file mode 100644 index 0000000..9bc63c2 --- /dev/null +++ b/test/teos/e2e/test_basic_e2e.py @@ -0,0 +1,389 @@ +from time import sleep +from riemann.tx import Tx +from binascii import hexlify +from coincurve import PrivateKey + +from cli import teos_cli, DATA_DIR, DEFAULT_CONF, CONF_FILE_NAME + +import common.cryptographer +from common.blob import Blob +from common.logger import Logger +from common.tools import compute_locator +from common.appointment import Appointment +from common.cryptographer import Cryptographer +from teos.utils.auth_proxy import JSONRPCException +from test.teos.e2e.conftest import ( + END_TIME_DELTA, + build_appointment_data, + get_random_value_hex, + create_penalty_tx, + run_teosd, + get_config, +) + +cli_config = get_config(DATA_DIR, CONF_FILE_NAME, DEFAULT_CONF) +common.cryptographer.logger = Logger(actor="Cryptographer", log_name_prefix="") + +teos_base_endpoint = "http://{}:{}".format(cli_config.get("TEOS_SERVER"), cli_config.get("TEOS_PORT")) +teos_add_appointment_endpoint = "{}/add_appointment".format(teos_base_endpoint) +teos_get_appointment_endpoint = "{}/get_appointment".format(teos_base_endpoint) + +# Run teosd +teosd_process = run_teosd() + +teos_pk, cli_sk, compressed_cli_pk = teos_cli.load_keys( + cli_config.get("TEOS_PUBLIC_KEY"), cli_config.get("CLI_PRIVATE_KEY"), cli_config.get("CLI_PUBLIC_KEY") +) + + +def broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, addr): + # Broadcast the commitment transaction and mine a block + bitcoin_cli.sendrawtransaction(commitment_tx) + bitcoin_cli.generatetoaddress(1, addr) + + +def get_appointment_info(locator, sk=cli_sk): + sleep(1) # Let's add a bit of delay so the state can be updated + return teos_cli.get_appointment(locator, sk, teos_pk, teos_base_endpoint) + + +def add_appointment(appointment_data, sk=cli_sk): + return teos_cli.add_appointment( + appointment_data, sk, teos_pk, teos_base_endpoint, cli_config.get("APPOINTMENTS_FOLDER_NAME") + ) + + +def test_commands_non_registered(bitcoin_cli, create_txs): + # All commands should failed if the user is not registered + + # Add appointment + commitment_tx, penalty_tx = create_txs + commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") + appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx) + + assert add_appointment(appointment_data) is False + + # Get appointment + assert get_appointment_info(appointment_data.get("locator")) is None + + +def test_commands_registered(bitcoin_cli, create_txs): + # Test registering and trying again + teos_cli.register(compressed_cli_pk, teos_base_endpoint) + + # Add appointment + commitment_tx, penalty_tx = create_txs + commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") + appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx) + + assert add_appointment(appointment_data) is True + + # Get appointment + r = get_appointment_info(appointment_data.get("locator")) + assert r.get("locator") == appointment_data.get("locator") + assert r.get("appointment").get("locator") == appointment_data.get("locator") + assert r.get("appointment").get("encrypted_blob") == appointment_data.get("encrypted_blob") + assert r.get("appointment").get("start_time") == appointment_data.get("start_time") + assert r.get("appointment").get("end_time") == appointment_data.get("end_time") + + +def test_appointment_life_cycle(bitcoin_cli, create_txs): + # First of all we need to register + # FIXME: requires register command in the cli + commitment_tx, penalty_tx = create_txs + commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") + appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx) + locator = compute_locator(commitment_tx_id) + + assert add_appointment(appointment_data) is True + + appointment_info = get_appointment_info(locator) + assert appointment_info is not None + assert appointment_info.get("status") == "being_watched" + + new_addr = bitcoin_cli.getnewaddress() + broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) + + appointment_info = get_appointment_info(locator) + assert appointment_info is not None + assert appointment_info.get("status") == "dispute_responded" + + # It can be also checked by ensuring that the penalty transaction made it to the network + penalty_tx_id = bitcoin_cli.decoderawtransaction(penalty_tx).get("txid") + + try: + bitcoin_cli.getrawtransaction(penalty_tx_id) + assert True + + except JSONRPCException: + # If the transaction is not found. + assert False + + # Now let's mine some blocks so the appointment reaches its end. + # Since we are running all the nodes remotely data may take more time than normal, and some confirmations may be + # missed, so we generate more than enough confirmations and add some delays. + for _ in range(int(1.5 * END_TIME_DELTA)): + sleep(1) + bitcoin_cli.generatetoaddress(1, new_addr) + + assert get_appointment_info(locator) is None + + +def test_appointment_malformed_penalty(bitcoin_cli, create_txs): + # Lets start by creating two valid transaction + commitment_tx, penalty_tx = create_txs + + # Now we can modify the penalty so it is invalid when broadcast + mod_penalty_tx = Tx.from_hex(penalty_tx) + tx_in = mod_penalty_tx.tx_ins[0].copy(redeem_script=b"") + mod_penalty_tx = mod_penalty_tx.copy(tx_ins=[tx_in]) + + commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") + appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, mod_penalty_tx.hex()) + locator = compute_locator(commitment_tx_id) + + assert add_appointment(appointment_data) is True + + # Broadcast the commitment transaction and mine a block + new_addr = bitcoin_cli.getnewaddress() + broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) + + # The appointment should have been removed since the penalty_tx was malformed. + sleep(1) + assert get_appointment_info(locator) is None + + +def test_appointment_wrong_key(bitcoin_cli, create_txs): + # This tests an appointment encrypted with a key that has not been derived from the same source as the locator. + # Therefore the tower won't be able to decrypt the blob once the appointment is triggered. + commitment_tx, penalty_tx = create_txs + + # The appointment data is built using a random 32-byte value. + appointment_data = build_appointment_data(bitcoin_cli, get_random_value_hex(32), penalty_tx) + + # We can't use teos_cli.add_appointment here since it computes the locator internally, so let's do it manually. + # We will encrypt the blob using the random value and derive the locator from the commitment tx. + appointment_data["locator"] = compute_locator(bitcoin_cli.decoderawtransaction(commitment_tx).get("txid")) + appointment_data["encrypted_blob"] = Cryptographer.encrypt(Blob(penalty_tx), get_random_value_hex(32)) + appointment = Appointment.from_dict(appointment_data) + + signature = Cryptographer.sign(appointment.serialize(), cli_sk) + data = {"appointment": appointment.to_dict(), "signature": signature} + + # Send appointment to the server. + response = teos_cli.post_request(data, teos_add_appointment_endpoint) + response_json = teos_cli.process_post_response(response) + + # Check that the server has accepted the appointment + signature = response_json.get("signature") + assert signature is not None + rpk = Cryptographer.recover_pk(appointment.serialize(), signature) + assert Cryptographer.verify_rpk(teos_pk, rpk) is True + assert response_json.get("locator") == appointment.locator + + # Trigger the appointment + new_addr = bitcoin_cli.getnewaddress() + broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) + + # The appointment should have been removed since the decryption failed. + sleep(1) + assert get_appointment_info(appointment.locator) is None + + +def test_two_identical_appointments(bitcoin_cli, create_txs): + # Tests sending two identical appointments to the tower. + # This tests sending an appointment with two valid transaction with the same locator. + # If they come from the same user, the last one will be kept + commitment_tx, penalty_tx = create_txs + commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") + + appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx) + locator = compute_locator(commitment_tx_id) + + # Send the appointment twice + assert add_appointment(appointment_data) is True + assert add_appointment(appointment_data) is True + + # Broadcast the commitment transaction and mine a block + new_addr = bitcoin_cli.getnewaddress() + broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) + + # The last appointment should have made it to the Responder + sleep(1) + appointment_info = get_appointment_info(locator) + + assert appointment_info is not None + assert appointment_info.get("status") == "dispute_responded" + assert appointment_info.get("appointment").get("penalty_rawtx") == penalty_tx + + +# FIXME: This test won't work since we're still passing appointment replicas to the Responder. +# Uncomment when #88 is addressed +# def test_two_identical_appointments_different_users(bitcoin_cli, create_txs): +# # Tests sending two identical appointments from different users to the tower. +# # This tests sending an appointment with two valid transaction with the same locator. +# # If they come from different users, both will be kept, but one will be dropped fro double-spending when passing to +# # the responder +# commitment_tx, penalty_tx = create_txs +# commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") +# +# appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx) +# locator = compute_locator(commitment_tx_id) +# +# # tmp keys from a different user +# tmp_sk = PrivateKey() +# tmp_compressed_pk = hexlify(tmp_sk.public_key.format(compressed=True)).decode("utf-8") +# teos_cli.register(tmp_compressed_pk, teos_base_endpoint) +# +# # Send the appointment twice +# assert add_appointment(appointment_data) is True +# assert add_appointment(appointment_data, sk=tmp_sk) is True +# +# # Check that we can get it from both users +# appointment_info = get_appointment_info(locator) +# assert appointment_info.get("status") == "being_watched" +# appointment_info = get_appointment_info(locator, sk=tmp_sk) +# assert appointment_info.get("status") == "being_watched" +# +# # Broadcast the commitment transaction and mine a block +# new_addr = bitcoin_cli.getnewaddress() +# broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) +# +# # The last appointment should have made it to the Responder +# sleep(1) +# appointment_info = get_appointment_info(locator) +# appointment_dup_info = get_appointment_info(locator, sk=tmp_sk) +# +# # One of the two request must be None, while the other must be valid +# assert (appointment_info is None and appointment_dup_info is not None) or ( +# appointment_dup_info is None and appointment_info is not None +# ) +# +# appointment_info = appointment_info if appointment_info is None else appointment_dup_info +# +# assert appointment_info.get("status") == "dispute_responded" +# assert appointment_info.get("appointment").get("penalty_rawtx") == penalty_tx + + +def test_two_appointment_same_locator_different_penalty_different_users(bitcoin_cli, create_txs): + # This tests sending an appointment with two valid transaction with the same locator. + commitment_tx, penalty_tx1 = create_txs + commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") + + # We need to create a second penalty spending from the same commitment + decoded_commitment_tx = bitcoin_cli.decoderawtransaction(commitment_tx) + new_addr = bitcoin_cli.getnewaddress() + penalty_tx2 = create_penalty_tx(bitcoin_cli, decoded_commitment_tx, new_addr) + + appointment1_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx1) + appointment2_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx2) + locator = compute_locator(commitment_tx_id) + + # tmp keys from a different user + tmp_sk = PrivateKey() + tmp_compressed_pk = hexlify(tmp_sk.public_key.format(compressed=True)).decode("utf-8") + teos_cli.register(tmp_compressed_pk, teos_base_endpoint) + + assert add_appointment(appointment1_data) is True + assert add_appointment(appointment2_data, sk=tmp_sk) is True + + # Broadcast the commitment transaction and mine a block + new_addr = bitcoin_cli.getnewaddress() + broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) + + # One of the transactions must have made it to the Responder while the other must have been dropped for + # double-spending + sleep(1) + appointment_info = get_appointment_info(locator) + appointment2_info = get_appointment_info(locator, sk=tmp_sk) + + # One of the two request must be None, while the other must be valid + assert (appointment_info is None and appointment2_info is not None) or ( + appointment2_info is None and appointment_info is not None + ) + + if appointment_info is None: + appointment_info = appointment2_info + appointment1_data = appointment2_data + + assert appointment_info.get("status") == "dispute_responded" + assert appointment_info.get("locator") == appointment1_data.get("locator") + + +def test_appointment_shutdown_teos_trigger_back_online(create_txs, bitcoin_cli): + global teosd_process + + teos_pid = teosd_process.pid + + commitment_tx, penalty_tx = create_txs + commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") + appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx) + locator = compute_locator(commitment_tx_id) + + assert add_appointment(appointment_data) is True + + # Restart teos + teosd_process.terminate() + teosd_process = run_teosd() + + assert teos_pid != teosd_process.pid + + # FIXME: We have to cheat here since users are not kept between restarts atm + sleep(1) + teos_cli.register(compressed_cli_pk, teos_base_endpoint) + + # Check that the appointment is still in the Watcher + appointment_info = get_appointment_info(locator) + + assert appointment_info is not None + assert appointment_info.get("status") == "being_watched" + + # Trigger appointment after restart + new_addr = bitcoin_cli.getnewaddress() + broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) + + # The appointment should have been moved to the Responder + sleep(1) + appointment_info = get_appointment_info(locator) + + assert appointment_info is not None + assert appointment_info.get("status") == "dispute_responded" + + +def test_appointment_shutdown_teos_trigger_while_offline(create_txs, bitcoin_cli): + global teosd_process + + teos_pid = teosd_process.pid + + commitment_tx, penalty_tx = create_txs + commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid") + appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx) + locator = compute_locator(commitment_tx_id) + + assert add_appointment(appointment_data) is True + + # Check that the appointment is still in the Watcher + appointment_info = get_appointment_info(locator) + assert appointment_info is not None + assert appointment_info.get("status") == "being_watched" + + # Shutdown and trigger + teosd_process.terminate() + new_addr = bitcoin_cli.getnewaddress() + broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) + + # Restart + teosd_process = run_teosd() + assert teos_pid != teosd_process.pid + + # FIXME: We have to cheat here since users are not kept between restarts atm + sleep(1) + teos_cli.register(compressed_cli_pk, teos_base_endpoint) + + # The appointment should have been moved to the Responder + appointment_info = get_appointment_info(locator) + + assert appointment_info is not None + assert appointment_info.get("status") == "dispute_responded" + + teosd_process.terminate()