Merge pull request #119 from orbitalturtle/cli_get_all_appointments

Add get_all_appointments cli command
This commit is contained in:
Sergi Delgado Segura
2020-04-20 14:15:31 +02:00
committed by GitHub
6 changed files with 193 additions and 7 deletions

View File

@@ -104,7 +104,17 @@ if `-f, --file` **is** specified, then the command expects a path to a json file
python teos_cli.py get_appointment <appointment_locator> python teos_cli.py get_appointment <appointment_locator>
### 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 ### help

View File

@@ -6,6 +6,7 @@ def show_usage():
"\n\tregister \tRegisters your user public key with the tower." "\n\tregister \tRegisters your user public key with the tower."
"\n\tadd_appointment \tRegisters a json formatted appointment 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_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\thelp \t\t\tShows a list of commands or help for a specific command."
"\n\nGLOBAL OPTIONS:" "\n\nGLOBAL OPTIONS:"
"\n\t-s, --server \tAPI server where to send the requests. Defaults to 'localhost' (modifiable in conf file)." "\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\nDESCRIPTION:"
"\n\n\tGets json formatted data about an appointment from the tower.\n" "\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"
)

View File

@@ -10,7 +10,7 @@ from getopt import getopt, GetoptError
from requests import ConnectTimeout, ConnectionError from requests import ConnectTimeout, ConnectionError
from requests.exceptions import MissingSchema, InvalidSchema, InvalidURL 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 from cli import DEFAULT_CONF, DATA_DIR, CONF_FILE_NAME, LOG_PREFIX
import common.cryptographer import common.cryptographer
@@ -175,6 +175,39 @@ def get_appointment(locator, cli_sk, teos_pk, teos_url):
return response_json 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): def load_keys(teos_pk_path, cli_sk_path, cli_pk_path):
""" """
Loads all the keys required so sign, send, and verify the appointment. 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: if appointment_data:
print(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": elif command == "help":
if args: if args:
command = args.pop(0) command = args.pop(0)
@@ -442,6 +480,9 @@ def main(args, command_line_conf):
else: else:
logger.error("Unknown command. Use help to check the list of available commands") 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: else:
sys.exit(show_usage()) sys.exit(show_usage())
@@ -457,7 +498,7 @@ def main(args, command_line_conf):
if __name__ == "__main__": if __name__ == "__main__":
command_line_conf = {} command_line_conf = {}
commands = ["register", "add_appointment", "get_appointment", "help"] commands = ["register", "add_appointment", "get_appointment", "get_all_appointments", "help"]
try: try:
opts, args = getopt(argv[1:], "s:p:h", ["server", "port", "help"]) opts, args = getopt(argv[1:], "s:p:h", ["server", "port", "help"])

View File

@@ -4,7 +4,7 @@ import shutil
import responses import responses
from binascii import hexlify from binascii import hexlify
from coincurve import PrivateKey from coincurve import PrivateKey
from requests.exceptions import ConnectionError from requests.exceptions import ConnectionError, Timeout
import common.cryptographer import common.cryptographer
from common.logger import Logger 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) add_appointment_endpoint = "{}/add_appointment".format(teos_url)
register_endpoint = "{}/register".format(teos_url) register_endpoint = "{}/register".format(teos_url)
get_appointment_endpoint = "{}/get_appointment".format(teos_url) get_appointment_endpoint = "{}/get_appointment".format(teos_url)
get_all_appointments_endpoint = "{}/get_all_appointments".format(teos_url)
dummy_appointment_data = { dummy_appointment_data = {
"tx": get_random_value_hex(192), "tx": get_random_value_hex(192),
@@ -194,7 +195,7 @@ def test_post_request():
@responses.activate @responses.activate
def test_process_post_response(): def test_process_post_response():
# Let's first crete a response # Let's first create a response
response = { response = {
"locator": dummy_appointment.to_dict()["locator"], "locator": dummy_appointment.to_dict()["locator"],
"signature": get_signature(dummy_appointment.serialize(), dummy_teos_sk), "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]) assert any([dummy_appointment.locator in f for f in files])
shutil.rmtree(appointments_folder) 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

View File

@@ -38,7 +38,7 @@ def prng_seed():
def setup_node(bitcoin_cli): def setup_node(bitcoin_cli):
# This method will create a new address a mine bitcoin so the node can be used for testing # This method will create a new address a mine bitcoin so the node can be used for testing
new_addr = bitcoin_cli.getnewaddress() new_addr = bitcoin_cli.getnewaddress()
bitcoin_cli.generatetoaddress(101, new_addr) bitcoin_cli.generatetoaddress(106, new_addr)
@pytest.fixture() @pytest.fixture()
@@ -60,6 +60,31 @@ def create_txs(bitcoin_cli):
return signed_commitment_tx, signed_penalty_tx 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(): def run_teosd():
teosd_process = Process(target=main, kwargs={"command_line_conf": {}}, daemon=True) teosd_process = Process(target=main, kwargs={"command_line_conf": {}}, daemon=True)
teosd_process.start() teosd_process.start()

View File

@@ -1,3 +1,4 @@
import json
from time import sleep from time import sleep
from riemann.tx import Tx from riemann.tx import Tx
from binascii import hexlify 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_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_add_appointment_endpoint = "{}/add_appointment".format(teos_base_endpoint)
teos_get_appointment_endpoint = "{}/get_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 # Run teosd
teosd_process = 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): def test_commands_non_registered(bitcoin_cli, create_txs):
# All commands should fail if the user is not registered # 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 is not None
assert appointment_info.get("status") == "being_watched" 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() new_addr = bitcoin_cli.getnewaddress()
broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) 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 is not None
assert appointment_info.get("status") == "dispute_responded" 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 # 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") 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 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): def test_appointment_malformed_penalty(bitcoin_cli, create_txs):
# Lets start by creating two valid transaction # Lets start by creating two valid transaction
commitment_tx, penalty_tx = create_txs commitment_tx, penalty_tx = create_txs