diff --git a/cli/README.md b/cli/README.md index 61b9082..a91d304 100644 --- a/cli/README.md +++ b/cli/README.md @@ -104,8 +104,18 @@ if `-f, --file` **is** specified, then the command expects a path to a json file python teos_cli.py get_appointment +### get_all_appointments + +This command is used to get information about all the appointments stored in a Eye of Satoshi tower. + +**Responses** + +This command returns all appointments stored in the watchtower. More precisely, it returns all the "response_trackers" and "watchtower_appointments" in a dictionary. + +#### Usage + + python teos_cli.py get_all_appointments - ### help Shows the list of commands or help about how to run a specific command. @@ -161,4 +171,4 @@ python teos_cli.py -s https://teosmainnet.pisa.watch add_appointment -f dummy_ap You can also change the config file to avoid specifying the server every time: -`TEOS_SERVER = "https://teosmainnet.pisa.watch"` \ No newline at end of file +`TEOS_SERVER = "https://teosmainnet.pisa.watch"` diff --git a/cli/help.py b/cli/help.py index 4ecf172..f9709c9 100644 --- a/cli/help.py +++ b/cli/help.py @@ -6,6 +6,7 @@ def show_usage(): "\n\tregister \tRegisters your user public key with the tower." "\n\tadd_appointment \tRegisters a json formatted appointment with the tower." "\n\tget_appointment \tGets json formatted data about an appointment from the tower." + "\n\tget_all_appointments \tGets information about all appointments stored in the tower." "\n\thelp \t\t\tShows a list of commands or help for a specific command." "\n\nGLOBAL OPTIONS:" "\n\t-s, --server \tAPI server where to send the requests. Defaults to 'localhost' (modifiable in conf file)." @@ -51,3 +52,14 @@ def help_get_appointment(): "\n\nDESCRIPTION:" "\n\n\tGets json formatted data about an appointment from the tower.\n" ) + + +def help_get_all_appointments(): + return ( + "NAME:" + "\tpython teos_cli get_all_appointments - Gets information about all appointments stored in the tower." + "\n\nUSAGE:" + "\tpython teos_cli.py get_all_appointments" + "\n\nDESCRIPTION:" + "\n\n\tGets information about all appointments stored in the tower.\n" + ) diff --git a/cli/teos_cli.py b/cli/teos_cli.py index b53f8c3..a267e3a 100644 --- a/cli/teos_cli.py +++ b/cli/teos_cli.py @@ -10,7 +10,7 @@ from getopt import getopt, GetoptError from requests import ConnectTimeout, ConnectionError from requests.exceptions import MissingSchema, InvalidSchema, InvalidURL -from cli.help import show_usage, help_add_appointment, help_get_appointment, help_register +from cli.help import show_usage, help_add_appointment, help_get_appointment, help_register, help_get_all_appointments from cli import DEFAULT_CONF, DATA_DIR, CONF_FILE_NAME, LOG_PREFIX import common.cryptographer @@ -175,6 +175,39 @@ def get_appointment(locator, cli_sk, teos_pk, teos_url): return response_json +def get_all_appointments(teos_url): + """ + Gets information about all appointments stored in the tower, if the user requesting the data is an administrator. + + Args: + teos_url (:obj:`str`): the teos base url. + + Returns: + :obj:`dict` a dictionary containing all the appointments stored by the Responder and Watcher if the tower + responds. + """ + + get_all_appointments_endpoint = "{}/get_all_appointments".format(teos_url) + + try: + response = requests.get(url=get_all_appointments_endpoint, timeout=5) + + if response.status_code != constants.HTTP_OK: + logger.error("The server returned an error", status_code=response.status_code, reason=response.reason) + return None + + response_json = json.dumps(response.json(), indent=4, sort_keys=True) + return response_json + + except ConnectionError: + logger.error("Can't connect to the Eye of Satoshi's API. Server cannot be reached") + return None + + except requests.exceptions.Timeout: + logger.error("The request timed out") + return None + + def load_keys(teos_pk_path, cli_sk_path, cli_pk_path): """ Loads all the keys required so sign, send, and verify the appointment. @@ -426,6 +459,11 @@ def main(args, command_line_conf): if appointment_data: print(appointment_data) + elif command == "get_all_appointments": + appointment_data = get_all_appointments(teos_url) + if appointment_data: + print(appointment_data) + elif command == "help": if args: command = args.pop(0) @@ -442,6 +480,9 @@ def main(args, command_line_conf): else: logger.error("Unknown command. Use help to check the list of available commands") + elif command == "get_all_appointments": + sys.exit(help_get_all_appointments()) + else: sys.exit(show_usage()) @@ -457,7 +498,7 @@ def main(args, command_line_conf): if __name__ == "__main__": command_line_conf = {} - commands = ["register", "add_appointment", "get_appointment", "help"] + commands = ["register", "add_appointment", "get_appointment", "get_all_appointments", "help"] try: opts, args = getopt(argv[1:], "s:p:h", ["server", "port", "help"]) diff --git a/test/cli/unit/test_teos_cli.py b/test/cli/unit/test_teos_cli.py index 52a32d6..3d175f8 100644 --- a/test/cli/unit/test_teos_cli.py +++ b/test/cli/unit/test_teos_cli.py @@ -4,7 +4,7 @@ import shutil import responses from binascii import hexlify from coincurve import PrivateKey -from requests.exceptions import ConnectionError +from requests.exceptions import ConnectionError, Timeout import common.cryptographer from common.logger import Logger @@ -31,6 +31,7 @@ teos_url = "http://{}:{}".format(config.get("TEOS_SERVER"), config.get("TEOS_POR add_appointment_endpoint = "{}/add_appointment".format(teos_url) register_endpoint = "{}/register".format(teos_url) get_appointment_endpoint = "{}/get_appointment".format(teos_url) +get_all_appointments_endpoint = "{}/get_all_appointments".format(teos_url) dummy_appointment_data = { "tx": get_random_value_hex(192), @@ -194,7 +195,7 @@ def test_post_request(): @responses.activate def test_process_post_response(): - # Let's first crete a response + # Let's first create a response response = { "locator": dummy_appointment.to_dict()["locator"], "signature": get_signature(dummy_appointment.serialize(), dummy_teos_sk), @@ -257,3 +258,37 @@ def test_save_appointment_receipt(monkeypatch): assert any([dummy_appointment.locator in f for f in files]) shutil.rmtree(appointments_folder) + + +@responses.activate +def test_get_all_appointments(): + # Response of get_all_appointments endpoint is all appointments from watcher and responder. + dummy_appointment_dict["status"] = "being_watched" + response = {"watcher_appointments": dummy_appointment_dict, "responder_trackers": {}} + + request_url = get_all_appointments_endpoint + responses.add(responses.GET, request_url, json=response, status=200) + result = teos_cli.get_all_appointments(teos_url) + + assert len(responses.calls) == 1 + assert responses.calls[0].request.url == request_url + assert json.loads(result).get("locator") == response.get("locator") + + +@responses.activate +def test_get_all_appointments_err(): + # Test that get_all_appointments handles a connection error appropriately. + request_url = get_all_appointments_endpoint + responses.add(responses.GET, request_url, body=ConnectionError()) + + assert not teos_cli.get_all_appointments(teos_url) + + # Test that get_all_appointments handles a timeout error appropriately. + responses.replace(responses.GET, request_url, body=Timeout()) + + assert not teos_cli.get_all_appointments(teos_url) + + # Test that get_all_appointments handles a 404 error appropriately. + responses.replace(responses.GET, request_url, status=404) + + assert teos_cli.get_all_appointments(teos_url) is None diff --git a/test/teos/e2e/conftest.py b/test/teos/e2e/conftest.py index eb892c0..38c6485 100644 --- a/test/teos/e2e/conftest.py +++ b/test/teos/e2e/conftest.py @@ -38,7 +38,7 @@ def prng_seed(): def setup_node(bitcoin_cli): # This method will create a new address a mine bitcoin so the node can be used for testing new_addr = bitcoin_cli.getnewaddress() - bitcoin_cli.generatetoaddress(101, new_addr) + bitcoin_cli.generatetoaddress(106, new_addr) @pytest.fixture() @@ -60,6 +60,31 @@ def create_txs(bitcoin_cli): return signed_commitment_tx, signed_penalty_tx +@pytest.fixture() +def create_five_txs(bitcoin_cli): + utxos = bitcoin_cli.listunspent() + + signed_commitment_txs = [] + signed_penalty_txs = [] + + for i in range(5): + if len(utxos) == 0: + raise ValueError("There're no UTXOs.") + + utxo = utxos.pop(0) + while utxo.get("amount") < Decimal(2 / pow(10, 5)): + utxo = utxos.pop(0) + + signed_commitment_tx = create_commitment_tx(bitcoin_cli, utxo) + + signed_commitment_txs.append(signed_commitment_tx) + decoded_commitment_tx = bitcoin_cli.decoderawtransaction(signed_commitment_tx) + + signed_penalty_txs.append(create_penalty_tx(bitcoin_cli, decoded_commitment_tx)) + + return signed_commitment_txs, signed_penalty_txs + + def run_teosd(): teosd_process = Process(target=main, kwargs={"command_line_conf": {}}, daemon=True) teosd_process.start() diff --git a/test/teos/e2e/test_basic_e2e.py b/test/teos/e2e/test_basic_e2e.py index ab47c49..8b13ab3 100644 --- a/test/teos/e2e/test_basic_e2e.py +++ b/test/teos/e2e/test_basic_e2e.py @@ -1,3 +1,4 @@ +import json from time import sleep from riemann.tx import Tx from binascii import hexlify @@ -27,6 +28,7 @@ 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) +teos_get_all_appointments_endpoint = "{}/get_all_appointments".format(teos_base_endpoint) # Run teosd teosd_process = run_teosd() @@ -53,6 +55,11 @@ def add_appointment(appointment_data, sk=cli_sk): ) +def get_all_appointments(): + r = teos_cli.get_all_appointments(teos_base_endpoint) + return json.loads(r) + + def test_commands_non_registered(bitcoin_cli, create_txs): # All commands should fail if the user is not registered @@ -101,6 +108,11 @@ def test_appointment_life_cycle(bitcoin_cli, create_txs): assert appointment_info is not None assert appointment_info.get("status") == "being_watched" + all_appointments = get_all_appointments() + watching = all_appointments.get("watcher_appointments") + responding = all_appointments.get("responder_trackers") + assert len(watching) == 1 and len(responding) == 0 + new_addr = bitcoin_cli.getnewaddress() broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) @@ -108,6 +120,11 @@ def test_appointment_life_cycle(bitcoin_cli, create_txs): assert appointment_info is not None assert appointment_info.get("status") == "dispute_responded" + all_appointments = get_all_appointments() + watching = all_appointments.get("watcher_appointments") + responding = all_appointments.get("responder_trackers") + assert len(watching) == 0 and len(responding) == 1 + # 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") @@ -129,6 +146,52 @@ def test_appointment_life_cycle(bitcoin_cli, create_txs): assert get_appointment_info(locator) is None +def test_multiple_appointments_life_cycle(bitcoin_cli, create_five_txs): + # Tests that get_all_appointments returns all the appointments the tower is storing at various stages in the appointment lifecycle. + appointments = [] + + commitment_txs, penalty_txs = create_five_txs + + # Create five appointments. + for i in range(5): + appointment = {} + + appointment["commitment_tx"] = commitment_txs[i] + appointment["penalty_tx"] = penalty_txs[i] + commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_txs[i]).get("txid") + appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_txs[i]) + appointment["appointment_data"] = appointment_data + locator = compute_locator(commitment_tx_id) + appointment["locator"] = locator + + appointments.append(appointment) + + # Send all of them to watchtower. + for appt in appointments: + add_appointment(appt.get("appointment_data")) + + # Two of these appointments are breached, and the watchtower responds to them. + for i in range(2): + new_addr = bitcoin_cli.getnewaddress() + broadcast_transaction_and_mine_block(bitcoin_cli, appointments[i]["commitment_tx"], new_addr) + bitcoin_cli.generatetoaddress(3, new_addr) + sleep(1) + + # Test that they all show up in get_all_appointments at the correct stages. + all_appointments = get_all_appointments() + watching = all_appointments.get("watcher_appointments") + responding = all_appointments.get("responder_trackers") + assert len(watching) == 3 and len(responding) == 2 + + # Now let's mine some blocks so these appointments reach the end of their lifecycle. + # 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. + new_addr = bitcoin_cli.getnewaddress() + for _ in range(int(1.5 * END_TIME_DELTA) + 5): + sleep(1) + bitcoin_cli.generatetoaddress(1, new_addr) + + def test_appointment_malformed_penalty(bitcoin_cli, create_txs): # Lets start by creating two valid transaction commitment_tx, penalty_tx = create_txs