def send(self, data, bid):
        """Send the transmission order

        Args:
            data : Data as bytes array to broadcast over satellite
            bid  : Bid in msats

        Returns:
            Dictionary with order metadata

        """
        assert (isinstance(data, bytes))

        # Post request to the API
        r = requests.post(self.server + '/order',
                          data={'bid': bid},
                          files={'file': data},
                          cert=(self.tls_cert, self.tls_key))

        # In case of failure, check the API error message
        if (r.status_code != requests.codes.ok
                and r.headers['content-type'] == "application/json"):
            self._print_errors(r)

        # Raise error if response status indicates failure
        r.raise_for_status()

        # Save the UUID and authentication token from the response
        res = r.json()
        self.uuid = res['uuid']
        self.auth_token = res['auth_token']

        # Print the response
        logger.debug("API Response:")
        logger.debug(json.dumps(res, indent=4, sort_keys=True))

        logger.info("Data successfully queued for transmission\n")
        print("--\nUUID:\n%s" % (res["uuid"]))
        print("--\nAuthentication Token:\n%s" % (res["auth_token"]))

        if ("lightning_invoice" in res):
            print("--\nAmount Due:\n%s millisatoshis\n" %
                  (res["lightning_invoice"]["msatoshi"]))
            print("--\nLightning Invoice Number:\n%s" %
                  (res["lightning_invoice"]["payreq"]))

        return res


def get_data(self):
        """Get data content sent over an API order"""
        logger.debug("Fetch message #%s from API" % (self.seq_num))

        r = requests.get(self.server + '/message/' + str(self.seq_num),
                         cert=(self.tls_cert, self.tls_key))

        r.raise_for_status()

        if (r.status_code == requests.codes.ok):
            data = r.content
            return data

def
get_data
(
self
)
:
logger
.
debug
(
"Fetch message #%s from API"
%
(
self
.
seq_num
)
)
r
=
requests
.
get
(
self
.
server
+
'/message/'
+
str
(
self
.
seq_num
)
,
cert
=
(
self
.
tls_cert
,
self
.
tls_key
)
)
r
.
raise_for_status
(
)
if
(
r
.
status_code
==
requests
.
codes
.
ok
)
:
data
=
r
.
content
return
data


Wait until the order achieves a target state ( or states )

def wait_state(self, target, timeout=120):
        """Wait until the order achieves a target state (or states)

        Args:
            target : String or list of strings with the state(s) to wait
                     for. When given as a list, this function waits until any
                     of the states is achieved.
            timeout : Timeout in seconds.

        Returns:
            (bool) Whether the state was successfully reached.

        """
        assert (isinstance(target, str) or isinstance(target, list))

        state_seen = {
            'pending': False,
            'paid': False,
            'transmitting': False,
            'sent': False,
            'received': False,
            'cancelled': False,
            'expired': False
        }
        msg = {
            'pending': "- Waiting for payment confirmation...",
            'paid': "- Payment confirmed. Ready to launch transmission...",
            'transmitting': "- Order in transmission...",
            'sent': "- Order successfully transmitted",
            'received': "- Reception confirmed by the ground station",
            'cancelled': "- Transmission cancelled",
            'expired': "- Order expired"
        }
        requires = {
            'pending': [],
            'paid': ['pending'],
            'transmitting': ['pending', 'paid'],
            'sent': ['pending', 'paid', 'transmitting'],
            'received': ['pending', 'paid', 'transmitting', 'sent'],
            'cancelled': ['pending'],
            'expired': ['pending']
        }

        if (not isinstance(target, list)):
            target = [target]
        assert ([state in state_seen.keys() for state in target])

        s_time = time.time()
        while (True):
            self._fetch()

            if (not state_seen[self.order['status']]):
                state_seen[self.order['status']] = True

                # If the status is not polled fast enough, some intermediate
                # states may not be seen. Mark the implied states as observed
                # and print their messages.
                for prereq_state in requires[self.order['status']]:
                    if (not state_seen[prereq_state]):
                        state_seen[prereq_state] = True
                        print(msg[prereq_state])

                # Print the current state
                print(msg[self.order['status']])

            if (any([state_seen[x] for x in target])):
                break

            c_time = time.time()
            if ((c_time - s_time) > timeout):
                print("Timeout")
                break

            time.sleep(0.5)

        return ('status' in self.order and self.order['status'] in target)


def confirm_tx(self, regions):
        """Confirm transmission of an API message

        Args:
            regions : Regions that were covered by the transmission

        """
        assert (self.seq_num is not None)

        if ((regions is None) or (self.tls_cert is None)
                or (self.tls_key is None)):
            return

        assert (isinstance(regions, list))

        logger.info("Confirm transmission of message {} on regions {}".format(
            self.seq_num, regions))

        r = requests.post(self.server + '/order/tx/' + str(self.seq_num),
                          data={'regions': json.dumps(regions)},
                          cert=(self.tls_cert, self.tls_key))

        if not r.ok:
            logger.error("Failed to confirm Tx of message {} "
                         "[status code {}]".format(self.seq_num,
                                                   r.status_code))
            self._print_errors(r)
        else:
            logger.info("Server response: " + r.json()['message'])


def confirm_rx(self, region):
        """Confirm reception of an API message

        Args:
            region : Coverage region

        """
        assert (self.seq_num is not None)

        if ((region is None) or (self.tls_cert is None)
                or (self.tls_key is None)):
            return

        logger.info("Confirm reception of API message {} on region {}".format(
            self.seq_num, region))

        r = requests.post(self.server + '/order/rx/' + str(self.seq_num),
                          data={'region': region},
                          cert=(self.tls_cert, self.tls_key))

        if not r.ok:
            logger.error("Failed to confirm Rx of message {} "
                         "[status code {}]".format(self.seq_num,
                                                   r.status_code))
            self._print_errors(r)
        else:
            logger.info("Server response: " + r.json()['message'])


def bump(self, bid=None):
        """Bump the order

        Args:
            bid : New bid in msats. If not defined, prompt user.

        Returns:
            Dictionary with order metadata

        """
        if (not self.order):
            self._fetch()

        if (self.order["status"] == "transmitting"):
            raise ValueError("Cannot bump order - already in transmission")

        if (self.order["status"] == "sent"
                or self.order["status"] == "received"):
            raise ValueError("Cannot bump order - already transmitted")

        if (self.order["status"] == "cancelled"
                or self.order["status"] == "expired"):
            raise ValueError("Order already {}".format(self.order["status"]))

        if (self.order["unpaid_bid"] > 0):
            unpaid_bid_msg = "(%d msat paid, %d msat unpaid)" % (
                self.order["bid"], self.order["unpaid_bid"])
        else:
            unpaid_bid_msg = ""

        previous_bid = self.order["bid"] + self.order["unpaid_bid"]
        tx_len = pkt.calc_ota_msg_len(self.order["message_size"])

        logger.info("Previous bid was {:d} msat for {:d} bytes {}".format(
            previous_bid, tx_len, unpaid_bid_msg))

        logger.info("Paid bid ratio is currently {:.2f} msat/byte".format(
            self.order["bid_per_byte"]))
        logger.info("Total (paid + unpaid) bid ratio is currently {:.2f} "
                    "msat/byte".format(float(previous_bid) / tx_len))

        if (bid is None):
            # Ask for new bid
            bid = bidding.ask_bid(tx_len, previous_bid)

        assert (isinstance(bid, int))

        # Post bump request
        r = requests.post(self.server + '/order/' + self.uuid + "/bump",
                          data={
                              'bid_increase': bid - previous_bid,
                              'auth_token': self.auth_token
                          },
                          cert=(self.tls_cert, self.tls_key))

        if (r.status_code != requests.codes.ok):
            self._print_errors(r)

        r.raise_for_status()

        # Print the response
        logger.info("Order bumped successfully\n")

        print("--\nNew Lightning Invoice Number:\n%s\n" %
              (r.json()["lightning_invoice"]["payreq"]))
        print("--\nNew Amount Due:\n%s millisatoshis\n" %
              (r.json()["lightning_invoice"]["msatoshi"]))

        logger.debug("API Response:")
        logger.debug(json.dumps(r.json(), indent=4, sort_keys=True))

        return r.json()


Delete the order

        Returns:
            API response

def delete(self):
        """Delete the order

        Returns:
            API response

        """
        if (not self.order):
            self._fetch()

        # Post delete request
        r = requests.delete(self.server + '/order/' + self.uuid,
                            headers={'X-Auth-Token': self.auth_token},
                            cert=(self.tls_cert, self.tls_key))

        if (r.status_code != requests.codes.ok):
            self._print_errors(r)

        r.raise_for_status()

        # Print the response
        logger.info("Order deleted successfully")

        logger.debug("API Response:")
        logger.debug(json.dumps(r.json(), indent=4, sort_keys=True))

        return r.json()

def
delete
(
self
)
:
if
(
not
self
.
order
)
:
self
.
_fetch
(
)
# Post delete request
r
=
requests
.
delete
(
self
.
server
+
'/order/'
+
self
.
uuid
,
headers
=
{
'X-Auth-Token'
:
self
.
auth_token
}
,
cert
=
(
self
.
tls_cert
,
self
.
tls_key
)
)
if
(
r
.
status_code
!=
requests
.
codes
.
ok
)
:
self
.
_print_errors
(
r
)
r
.
raise_for_status
(
)
# Print the response
logger
.
info
(
"Order deleted successfully"
)
logger
.
debug
(
"API Response:"
)
logger
.
debug
(
json
.
dumps
(
r
.
json
(
)
,
indent
=
4
,
sort_keys
=
True
)
)
return
r
.
json
(
)


def suggest_bid(data_size, prev_bid=None):
    """Suggest a valid bid for the given data transmission length

    Args:
        data_size : Size of the transmit data in bytes
        prev_bid  : Previous bid, if any

    Returns:
        Bid in millisatoshis

    """
    assert (isinstance(data_size, int))

    if (prev_bid is not None):
        # Suggest a 5% higher msat/byte ratio
        suggested_bid = ceil(DEFAULT_BUMP_FACTOR * prev_bid)
    else:
        suggested_bid = max(ceil(data_size * MIN_BID_PER_BYTE), MIN_BID)
    return suggested_bid

def
suggest_bid
(
data_size
,
prev_bid
=
)
:
assert
(
isinstance
(
data_size
,
int
)
)
if
(
prev_bid
is
not
)
:
# Suggest a 5% higher msat/byte ratio
suggested_bid
=
ceil
(
DEFAULT_BUMP_FACTOR
*
prev_bid
)
else
:
suggested_bid
=
max
(
ceil
(
data_size
*
MIN_BID_PER_BYTE
)
,
MIN_BID
)
return
suggested_bid


def validate_bid(bid, prev_bid=None):
    """Validate a given bid

    Args:
        bid      : New bid in msats
        prev_bid : Previous bid in msats, used if bumping

    Returns:
        Bool indicating whether the bid is valid.

    """
    if (bid <= 0):
        print("Please provide a positive bid in millisatoshis")
        return False

    if (prev_bid is not None and bid <= prev_bid):
        print("Please provide a bid higher than the previous bid")
        return False

    if (bid > MAX_BID):
        print("Bid too large. Are you sure you want to bid {:.2f} "
              "sats (satoshis)?".format(bid / 1e3))
        return False

    return True


def ask_bid(data_size, prev_bid=None):
    """Prompt user for a bid to transmit the given data size

    Args:
        data_size : Size of the transmit data in bytes
        prev_bid  : Previous bid, if any

    Returns:
        Bid in millisatoshis

    """
    suggested_bid = suggest_bid(data_size, prev_bid)

    print("")
    msg = "Your {}bid to transmit {:d} bytes (in millisatoshis)".format(
        ("new total " if prev_bid is not None else ""), data_size)

    while (True):
        bid = util.typed_input(msg, default=suggested_bid)
        if (validate_bid(bid, prev_bid)):
            break

    if (prev_bid is not None):
        logger.info("Bump bid by %d msat to a total of %d msat "
                    "(%.2f msat/byte)" %
                    (bid - prev_bid, bid, float(bid) / data_size))
    else:
        logger.info("Post data with bid of %d millisatoshis "
                    "(%.2f msat/byte)" % (bid, float(bid) / data_size))

    return bid


def __init__(self,
                 server,
                 socks,
                 kbps,
                 tx_event,
                 channel,
                 regions=None,
                 tls_cert=None,
                 tls_key=None):
        """ DemoRx Constructor

        Args:
            server   : API server address where the order lives
            socks    : Instances of UdpSock over which to send the packets
            kbps     : Target bit rate in kbps
            tx_event : SSE event to use as trigger for transmissions
            channel  : API channel number
            regions  : Regions covered by the transmission (for Tx
                       confirmation)
            tls_key  : API client key (for Tx confirmation)
            tls_cer  : API client certificate (for Tx confirmation)

        """
        # Validate args
        assert (isinstance(socks, list))
        assert (all([isinstance(x, net.UdpSock) for x in socks]))

        # Configs
        self.server = server
        self.socks = socks
        self.kbps = kbps
        self.tx_event = tx_event
        self.channel = channel
        self.regions = regions
        self.tls_cert = tls_cert
        self.tls_key = tls_key

        # State
        self.last_seq_num = None

def
__init__
(
self
,
server
,
socks
,
kbps
,
tx_event
,
channel
,
regions
=
,
tls_cert
=
,
tls_key
=
)
:
# Validate args
assert
(
isinstance
(
socks
,
list
)
)
assert
(
all
(
[
isinstance
(
x
,
net
.
UdpSock
)
for
x
in
socks
]
)
)
# Configs
self
.
server
=
server
self
.
socks
=
socks
self
.
kbps
=
kbps
self
.
tx_event
=
tx_event
self
.
channel
=
channel
self
.
regions
=
regions
self
.
tls_cert
=
tls_cert
self
.
tls_key
=
tls_key
# State
self
.
last_seq_num
=


def _send_pkts(self, pkts):
        """Transmit Blocksat packets of the API message over all sockets

        Transmit and sleep (i.e., block) to guarantee the target bit rate.

        Args:
            pkts : List of BlocksatPkt objects to be send over sockets

        """
        assert (isinstance(pkts, list))
        assert (all([isinstance(x, BlocksatPkt) for x in pkts]))

        byte_rate = self.kbps * 1e3 / 8  # bytes / sec
        next_tx = time.time()
        for i, pkt in enumerate(pkts):
            # Send the same packet on all sockets
            for sock in self.socks:
                sock.send(pkt.pack())
                logger.debug("Send packet %d - %d bytes" % (i, len(pkt)))

            # Throttle
            tx_delay = len(pkt) / byte_rate
            next_tx += tx_delay
            sleep = next_tx - time.time()
            if (sleep > 0):
                time.sleep(sleep)

def
_send_pkts
(
self
,
pkts
)
:
assert
(
isinstance
(
pkts
,
list
)
)
assert
(
all
(
[
isinstance
(
x
,
BlocksatPkt
)
for
x
in
pkts
]
)
)
byte_rate
=
self
.
kbps
*
1e3
/
8
# bytes / sec
next_tx
=
time
.
time
(
)
for
i
,
pkt
in
enumerate
(
pkts
)
:
# Send the same packet on all sockets
for
sock
in
self
.
socks
:
sock
.
send
(
pkt
.
pack
(
)
)
logger
.
debug
(
"Send packet %d - %d bytes"
%
(
i
,
len
(
pkt
)
)
)
# Throttle
tx_delay
=
len
(
pkt
)
/
byte_rate
next_tx
+=
tx_delay
sleep
=
next_tx
time
.
time
(
)
if
(
sleep
>
0
)
:
time
.
sleep
(
sleep
)


def _handle_event(self, event):
        """Handle event broadcast by the SSE server

        Args:
            event : Event generated by the sseclient

        """
        # Parse the order corresponding to the event
        order = json.loads(event.data)

        # Debug
        logger.debug("Order: " + json.dumps(order, indent=4, sort_keys=True))

        # Proceed when the event matches the target Tx trigger event
        if (order["status"] != self.tx_event):
            return

        # Sequence number
        seq_num = order["tx_seq_num"]

        # If the sequence number has rolled back, maybe it is because the
        # server has restarted the sequence numbers (not uncommon on test
        # environments). In this case, restart the sequence.
        if (self.last_seq_num is not None and seq_num < self.last_seq_num):
            logger.warning("Tx sequence number rolled back from {} to "
                           "{}".format(self.last_seq_num, seq_num))
            self.last_seq_num = None

        rx_pending = True
        while (rx_pending):
            # Receive all messages until caught up
            if (self.last_seq_num is None):
                next_seq_num = seq_num
            else:
                next_seq_num = self.last_seq_num + 1

            # Keep track of the last processed sequence number
            self.last_seq_num = next_seq_num

            # Is this an interation to catch up with a sequence
            # number gap or a normal transmission iteration?
            if (seq_num == next_seq_num):
                rx_pending = False
            else:
                logger.info("Catch up with transmission %d" % (next_seq_num))

            logger.info("Message %-5d\tSize: %d bytes\t" %
                        (next_seq_num, order["message_size"]))

            # Get the API message data
            order = ApiOrder(self.server,
                             seq_num=next_seq_num,
                             tls_cert=self.tls_cert,
                             tls_key=self.tls_key)
            data = order.get_data()

            if (data is None):
                # Empty message. There is nothing else to do.
                continue

            # Split API message data into Blocksat packet(s)
            tx_handler = BlocksatPktHandler()
            tx_handler.split(data, next_seq_num, self.channel)
            pkts = tx_handler.get_frags(next_seq_num)

            logger.debug("Transmission is going to take: "
                         "{:6.2f} sec".format(len(data) * 8 / (self.kbps)))

            # Send the packet(s)
            self._send_pkts(pkts)

            # Send transmission confirmation to the server
            order.confirm_tx(self.regions)


def run(self):
        """Run the demo-rx transmission loop"""
        logger.info("Connecting with Satellite API server...")
        while (True):
            try:
                # Server-sent Events (SSE) Client
                r = requests.get(self.server + "/subscribe/transmissions",
                                 stream=True,
                                 cert=(self.tls_cert, self.tls_key))
                r.raise_for_status()
                client = sseclient.SSEClient(r)
                logger.info("Connected. Waiting for events...\n")

                # Continuously wait for events
                for event in client.events():
                    self._handle_event(event)

            except requests.exceptions.ChunkedEncodingError as e:
                logger.debug(e)
                pass

            except requests.exceptions.ConnectionError as e:
                logger.debug(e)
                time.sleep(2)
                pass

            except requests.exceptions.RequestException as e:
                logger.debug(e)
                time.sleep(2)
                pass

            except KeyboardInterrupt:
                exit()

            logger.info("Reconnecting...")


def __init__(self, recv_once=False, recv_queue=None):
        """Constructor

        Args:
            recv_once : Receive a single message and stop
            recv_queue : Queue to mirror (store) the saved/serialized messages

        Note:
            The constructor options are useful for testing but are not
            necessary in production code.

        """
        self.enabled = True
        self.recv_once = recv_once
        assert (recv_queue is None or isinstance(recv_queue, queue.Queue))
        self.recv_queue = recv_queue

def __init__ ( self , recv_once = False , recv_queue = None ) : self . enabled = True self . recv_once = recv_once assert ( recv_queue is None or isinstance ( recv_queue , queue . Queue ) ) self . recv_queue = recv_queue


def run(self,
            gpg,
            download_dir,
            sock_addr,
            interface,
            channel,
            plaintext,
            save_raw,
            sender=None,
            stdout=False,
            no_save=False,
            echo=False,
            exec_cmd=None,
            gossip_opts=None,
            server_addr=None,
            tls_cert=None,
            tls_key=None,
            region=None):
        """Run loop

        Args:
            gpg          : Gnupg object
            download_dir : Directory where downloaded messages are saved
            sock_addr    : Multicast socket address to listen to
            interface    : Network interface to listen to
            channel      : Satellite API channel to listen to
            plaintext    : Receive messages in plaintext mode
            save_raw     : Save the raw message content without decapsulation
            sender       : Sender's GPG fingerprint for filtering of messages
            stdout       : Print messages to stdout instead of saving them
            no_save      : Don't save downloaded messages
            echo         : Echo text messages to stdout on reception
            exec_cmd     : Arbitrary shell command to run for each download
            gossip_opts  : Options for reception of Lightning gossip snapshots
            server_addr  : API server address for Rx confirmations
            tls_cert     : TLS client cert to authenticate on Rx confirmations
            tls_key      : TLS client key to authenticate on Rx confirmations
            region       : Satellite region to inform on Rx confirmations

        """
        logger.debug("Starting API listener")

        # Open UDP socket
        sock = net.UdpSock(sock_addr, interface)

        # Handler to collect groups of Blocksat packets that form an API
        # message
        pkt_handler = BlocksatPktHandler()

        # Set of decoded messages (to avoid repeated decoding)
        decoded_msgs = set()

        logger.info("Waiting for data...")
        while self.enabled:
            try:
                udp_payload, addr = sock.recv()
            except KeyboardInterrupt:
                break

            # Cast payload to BlocksatPkt object
            pkt = BlocksatPkt()
            pkt.unpack(udp_payload)

            # Filter API channel
            if (channel != ApiChannel.ALL.value and pkt.chan_num != channel):
                logger.debug("Packet discarded (channel {:d})".format(
                    pkt.chan_num))
                continue

            # Feed new packet into the packet handler
            all_frags_received = pkt_handler.append(pkt)

            # Decode each message only once
            seq_num = pkt.seq_num
            chan_seq_num = "{}-{}".format(pkt.chan_num, seq_num)
            if (chan_seq_num in decoded_msgs):
                logger.debug("Message {} from channel {} has already been "
                             "decoded".format(seq_num, pkt.chan_num))
                continue

            # Assume that the incoming message has forward error correction
            # (FEC) encoding. With FEC, the message may become decodable before
            # receiving all fragments (BlocksatPkts). For every packet, check
            # if the FEC data is decodable already and proceed with the
            # processing in the positive case. If the message is not actually
            # FEC-encoded, it will never assert the "fec_decodable" flag. In
            # this case, proceed with the processing only when all fragments
            # are received.
            msg = api_msg.ApiMsg(pkt_handler.concat(seq_num, force=True),
                                 msg_format="fec_encoded")
            fec_decodable = msg.is_fec_decodable()
            if (not fec_decodable and not all_frags_received):
                continue

            # API message is ready to be decoded
            if (channel == ApiChannel.ALL.value):
                logger.info("-------- API message {:d} (channel {:d})".format(
                    seq_num, pkt.chan_num))
            else:
                logger.info("-------- API message {:d}".format(seq_num))
            logger.debug("Message source: {}:{}".format(addr[0], addr[1]))
            logger.info("Fragments: {:d}".format(
                pkt_handler.get_n_frags(seq_num)))

            # Send confirmation of reception to API server
            order = ApiOrder(server_addr,
                             seq_num=seq_num,
                             tls_cert=tls_cert,
                             tls_key=tls_key)
            order.confirm_rx(region)

            # Decode the data from the available FEC chunks or from the
            # complete collection of Blocksat Packets
            if (fec_decodable):
                msg.fec_decode()
                data = msg.data['original']
            else:
                data = pkt_handler.concat(seq_num)

            # Mark as decoded
            decoded_msgs.add(chan_seq_num)

            # Delete message from the packet handler
            del pkt_handler.frag_map[seq_num]

            # Clean up old (timed-out) messages from the packet handler
            pkt_handler.clean()

            if (len(data) <= 0):
                logger.warning("Empty message")
                continue

            # The FEC-decoded data could be encrypted, signed, and/or
            # encapsulated. Decode it and obtain the original data.
            msg = api_msg.decode(data,
                                 plaintext=plaintext,
                                 decapsulate=(not save_raw),
                                 sender=sender,
                                 gpg=gpg)
            if (msg is None):
                continue

            # Finalize the processing of the decoded message
            if (stdout):
                msg.serialize()
            elif (not no_save):
                download_path = msg.save(download_dir)

            if (self.recv_queue is not None):
                self.recv_queue.put(msg.get_data(target='original'))

            if (echo):
                # Not all messages can be decoded in UTF-8 (binary files
                # cannot). Also, messages that were not sent in plaintext and
                # raw (non-encapsulated) format. Echo the results only when the
                # UTF-8 decoding works.
                try:
                    logger.info("Message:\n\n {} \n".format(
                        msg.data['original'].decode()))
                except UnicodeDecodeError:
                    logger.debug("Message not decodable in UFT-8")
            else:
                logger.debug("Message: {}".format(msg.data['original']))

            if (exec_cmd):
                cmd = shlex.split(
                    exec_cmd.replace("{}", shlex.quote(download_path)))
                logger.debug("Exec:\n> {}".format(" ".join(cmd)))
                subprocess.run(cmd)

            if (gossip_opts is not None):
                cmd = [
                    gossip_opts['cli'], 'snapshot', 'load',
                    shlex.quote(download_path)
                ]
                if (gossip_opts['dest'] is not None):
                    cmd.append(gossip_opts['dest'])

                logger.debug("Exec:\n> {}".format(" ".join(cmd)))
                subprocess.run(cmd)

            if (self.recv_once):
                self.stop()


def _is_gpg_keyring_set(gnupghome):
    """Check if the keyring is already configured

    It is configured when:
    - It has at least one private key
    - It has at least two public keys
    - One of the public keys is the blocksat pubkey

    """
    if not os.path.exists(gnupghome):
        return False

    gpg = Gpg(gnupghome)

    if (len(gpg.gpg.list_keys(True)) == 0):  # no private key
        return False

    if (len(gpg.gpg.list_keys()) < 2):  # no two public keys
        return False

    if (len(gpg.gpg.list_keys(
            keys=defs.blocksat_pubkey)) == 0):  # blocksat key
        return False

    return True

def
_is_gpg_keyring_set
(
gnupghome
)
:
if
not
os
.
path
.
exists
(
gnupghome
)
:
return
False
gpg
=
Gpg
(
gnupghome
)
if
(
len
(
gpg
.
gpg
.
list_keys
(
True
)
)
==
0
)
:
# no private key
return
False
if
(
len
(
gpg
.
gpg
.
list_keys
(
)
)
<
2
)
:
# no two public keys
return
False
if
(
len
(
gpg
.
gpg
.
list_keys
(
keys
=
defs
.
blocksat_pubkey
)
)
==
0
)
:
# blocksat key
return
False
return
True


def config_keyring(gpg, log_if_configured=False):
    """Configure the local keyring

    Create a keypair and import Blockstream's public key

    Args:
        gpg : Gpg object
        log_if_configured : Print log message if keyring is already configured

    """
    assert (isinstance(gpg, Gpg))
    if _is_gpg_keyring_set(gpg.gpghome):
        if (log_if_configured):
            logger.info("Keyring already configured")
        return

    # Generate new keypair
    logger.info("Generating a GPG keypair to encrypt and decrypt API messages")
    name = input("User name represented by the key: ")
    email = input("E-mail address: ")
    comment = input("Comment to attach to the user ID: ")
    gpg.create_keys(name, email, comment)

    # Import Blockstream's public key
    #
    # NOTE: the order is important here. Add Blockstream's public key only
    # after adding the user key. With that, the user key becomes the first key
    # on the keyring, which is used by default.
    pkg_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    key_path = os.path.join(pkg_dir, 'gpg', defs.blocksat_pubkey + ".gpg")

    with open(key_path) as fd:
        key_data = fd.read()

    import_result = gpg.gpg.import_keys(key_data)

    if (len(import_result.fingerprints) == 0):
        logger.warning("Failed to import key {}".format(defs.blocksat_pubkey))
        return

    logger.info("Imported key {}".format(import_result.fingerprints[0]))

    gpg.gpg.trust_keys(defs.blocksat_pubkey, 'TRUST_ULTIMATE')

    if (not _is_gpg_keyring_set(gpg.gpghome)):
        raise RuntimeError("GPG keyring configuration failed")


def __init__(self, gpghome, verbose=False, interactive=False):
        """Create GnuPG instance"""
        if (not os.path.exists(gpghome)):
            os.mkdir(gpghome)
            # Make sure only the owner has permissions to read, write, and
            # execute the GPG home directory
            os.chmod(gpghome, stat.S_IRWXU)

        self.interactive = interactive
        self.gpghome = gpghome
        self.passphrase = None

        # Create GPG object and fetch the list of current private keys
        self.gpg = gnupg.GPG(verbose=verbose, gnupghome=gpghome)


def _find_gpg_key_by_email(self, email):
        """Find GPG key with matching email address on UID"""

        # Fetch the list of current private keys
        current_priv_keys = self.gpg.list_keys(True)

        for key in current_priv_keys:
            for uid in key['uids']:
                if (email in uid):
                    return key


def create_keys(self, name, email, comment, passphrase=None):
        """Generate GPG Keys"""

        # Check if there is already a key with this e-mail address
        matching_key = self._find_gpg_key_by_email(email)
        if (matching_key):
            logger.info("Found existing key {} with uid {}".format(
                matching_key['fingerprint'], matching_key['uids']))

            if (not self.interactive):
                logger.info("Aborting")
                return
            elif (util.ask_yes_or_no("Abort the creation of a new key?")):
                return

        # Password
        if (passphrase is None):
            self.prompt_passphrase('Please enter a passphrase to protect '
                                   'your key: ')
        else:
            self.set_passphrase(passphrase)

        # Generate key
        key_params = self.gpg.gen_key_input(key_type="RSA",
                                            key_length=1024,
                                            name_real=name,
                                            name_comment=comment,
                                            name_email=email,
                                            passphrase=self.passphrase)
        self.gpg.gen_key(key_params)
        logger.info("Keys succesfully generated at {}".format(
            os.path.abspath(self.gpghome)))


def get_default_public_key(self):
        """Get info corresponding to the first public key on the keyring

        Returns:
            Dictionary with key information such as 'fingerprint', 'keyid', and
            'uids'.

        """
        return self.gpg.list_keys()[0]

def get_default_public_key ( self ) : return self . gpg . list_keys ( ) [ 0 ]


def get_default_priv_key(self):
        """Get info corresponding to the first private key on the keyring

        Returns:
            Dictionary with key information such as 'fingerprint', 'keyid', and
            'uids'.

        """
        return self.gpg.list_keys(True)[0]

def get_default_priv_key ( self ) : return self . gpg . list_keys ( True ) [ 0 ]


def get_public_key(self, fingerprint):
        """Find a specific public key on the keyring and return the info dict

        Returns:
            Dictionary with key information such as 'fingerprint', 'keyid', and
            'uids'.

        """
        key_map = self.gpg.list_keys().key_map
        assert(fingerprint in key_map), \
            "Could not find public key {}".format(fingerprint)
        return key_map[fingerprint]

def
get_public_key
(
self
,
fingerprint
)
:
key_map
=
self
.
gpg
.
list_keys
(
)
.
key_map
assert
(
fingerprint
in
key_map
)
,
"Could not find public key {}"
.
format
(
fingerprint
)
return
key_map
[
fingerprint
]


def get_priv_key(self, fingerprint):
        """Find a specific private key on the keyring and return the info dict

        Returns:
            Dictionary with key information such as 'fingerprint', 'keyid', and
            'uids'.
        """
        key_map = self.gpg.list_keys(True).key_map
        assert(fingerprint in key_map), \
            "Could not find private key {}".format(fingerprint)
        return key_map[fingerprint]

def
get_priv_key
(
self
,
fingerprint
)
:
key_map
=
self
.
gpg
.
list_keys
(
True
)
.
key_map
assert
(
fingerprint
in
key_map
)
,
"Could not find private key {}"
.
format
(
fingerprint
)
return
key_map
[
fingerprint
]


def encrypt(self, data, recipients, always_trust=False, sign=None):
        """Encrypt a given data array"""
        return self.gpg.encrypt(data,
                                recipients,
                                always_trust=always_trust,
                                sign=sign,
                                passphrase=self.passphrase)

def encrypt ( self , data , recipients , always_trust = False , sign = None ) : return self . gpg . encrypt ( data , recipients , always_trust = always_trust , sign = sign , passphrase = self . passphrase )


def decrypt(self, data):
        """Decrypt a given data array"""
        if (not self.interactive and self.passphrase is None):
            raise RuntimeError(
                "Passphrase must be defined in non-interactive mode")

        return self.gpg.decrypt(data, passphrase=self.passphrase)


def sign(self, data, keyid, clearsign=True, detach=False):
        """Sign a given data array"""
        assert(not (clearsign and detach)), \
            "clearsign and detach options are mutually exclusive"

        if (not self.interactive and self.passphrase is None):
            raise RuntimeError(
                "Passphrase must be defined in non-interactive mode")

        return self.gpg.sign(data,
                             keyid=keyid,
                             clearsign=clearsign,
                             detach=detach,
                             passphrase=self.passphrase)


Warn options overridden by arguments -- gossip and -- btc - src

def _warn_common_overrides(args):
    """Warn options overridden by arguments --gossip and --btc-src"""
    if (not (args.gossip or args.btc_src)):
        return

    opt = "--gossip" if args.gossip else "--btc-src"

    if (args.channel != ApiChannel.USER.value):
        logger.warning("Option {} overrides --channel".format(opt))

    if (args.server != server_map['main'] or args.net is not None):
        logger.warning("Option {} overrides --net and --server".format(opt))


def config(args):
    """API config command"""
    gnupghome = os.path.join(args.cfg_dir, args.gnupghome)
    gpg = Gpg(gnupghome, verbose=args.verbose, interactive=True)
    config_keyring(gpg, log_if_configured=True)

def config ( args ) : gnupghome = os . path . join ( args . cfg_dir , args . gnupghome ) gpg = Gpg ( gnupghome , verbose = args . verbose , interactive = True ) config_keyring ( gpg , log_if_configured = True )


def send(args):
    """Send a message over satellite"""
    gnupghome = os.path.join(args.cfg_dir, args.gnupghome)
    server_addr = _get_server_addr(args.net, args.server)

    # Instantiate the GPG wrapper object if running with encryption or signing
    if (not args.plaintext or args.sign):
        gpg = Gpg(gnupghome, interactive=True)
        config_keyring(gpg)
    else:
        gpg = None

    # A passphrase is required if signing
    if (args.sign and (not args.no_password)):
        gpg.prompt_passphrase('Password to private key used for message '
                              'signing: ')

    # File or text message to send over satellite
    if (args.file is None):
        if (args.message is None):
            data = input("Type a message: ").encode()
            print()
        else:
            data = args.message.encode()
        basename = None
    else:
        basename = os.path.basename(args.file)
        with open(args.file, 'rb') as f:
            data = f.read()

    assert(len(data) > 0), \
        "Empty {}".format("file" if args.file else "message")

    # Put file or text within an API message
    msg = api_msg.generate(data,
                           filename=basename,
                           plaintext=args.plaintext,
                           encapsulate=(not args.send_raw),
                           sign=args.sign,
                           fec=args.fec,
                           gpg=gpg,
                           recipient=args.recipient,
                           trust=args.trust,
                           sign_key=args.sign_key,
                           fec_overhead=args.fec_overhead)

    # Actual number of bytes used for satellite transmission
    tx_len = calc_ota_msg_len(msg.get_length())
    logger.info("Satellite transmission will use %d bytes" % (tx_len))

    # Ask user for bid or take it from argument
    bid = args.bid if args.bid else bidding.ask_bid(tx_len)

    # API transmission order
    order = ApiOrder(server_addr, tls_cert=args.tls_cert, tls_key=args.tls_key)
    res = order.send(msg.get_data(), bid)
    print()

    # The API servers (except the gossip and btc-src instances) should return a
    # Lightning invoice for the transmission order
    if (server_addr != server_map['gossip']
            and server_addr != server_map['btc-src']):
        payreq = res["lightning_invoice"]["payreq"]

        # Print QR code
        try:
            qr = qrcode.QRCode()
            qr.add_data(payreq)
            qr.print_ascii()
        except UnicodeError:
            qr.print_tty()

        # Execute arbitrary command with the Lightning invoice
        if (args.invoice_exec):
            cmd = shlex.split(args.invoice_exec.replace("{}", payreq))
            logger.debug("Execute:\n> {}".format(" ".join(cmd)))
            subprocess.run(cmd)

    # Wait until the transmission completes (after the ground station confirms
    # reception). Stop if it is canceled.
    if (args.no_wait):
        return

    try:
        target_state = ['sent', 'cancelled']
        order.wait_state(target_state)
    except KeyboardInterrupt:
        pass


def listen(args):
    """Listen to API messages received over satellite"""
    gnupghome = os.path.join(args.cfg_dir, args.gnupghome)
    download_dir = os.path.join(args.cfg_dir, "api", "downloads")

    # Override some options based on the mutual exclusive --gossip and
    # --btc-src arguments, if present
    if (args.gossip):
        channel = ApiChannel.GOSSIP.value
        server_addr = server_map['gossip']
        args.plaintext = True
    elif (args.btc_src):
        channel = ApiChannel.BTC_SRC.value
        server_addr = server_map['btc-src']
        args.plaintext = True
    else:
        channel = args.channel
        server_addr = _get_server_addr(args.net, args.server)

    historian_cli = os.path.join(args.historian_path, 'historian-cli') \
        if (args.historian_path) else 'historian-cli'

    # Argument validation and special cases
    if (args.gossip):
        if (which(historian_cli) is None):
            logger.error("Option --gossip requires the historian-cli "
                         "application but could not find it")
            logger.info("Use option --historian-path to specify the path to "
                        "historian-cli")
            raise ValueError("Could not find the historian-cli application")

        if (args.stdout):
            raise ValueError("Argument --gossip is not allowed with argument "
                             "--stdout")
        elif (args.no_save):
            raise ValueError("Argument --gossip is not allowed with argument "
                             "--no-save")

        gossip_opts = {
            'cli': historian_cli,
            'dest': args.historian_destination
        }
    else:
        gossip_opts = None

    _warn_common_overrides(args)  # warn args overridden by --gossip/--btc-src

    if (args.exec):
        # Do not support the option in plaintext mode
        if (args.plaintext):
            raise ValueError("Option --exec is not allowed in plaintext mode")

        # Require either --sender or --insecure
        if (args.sender is None and not args.insecure):
            raise ValueError("Option --exec requires a specified --sender")

        # Warn about unsafe commands
        base_exec_cmd = shlex.split(args.exec)[0]
        unsafe_cmds = ["/bin/bash", "bash", "/bin/sh", "sh", "rm"]
        if (base_exec_cmd in unsafe_cmds):
            logger.warning("Running {} on --exec is considered unsafe".format(
                base_exec_cmd))

    # GPG wrapper object
    if (not args.plaintext or args.sender):
        gpg = Gpg(gnupghome, interactive=True)
        config_keyring(gpg)
    else:
        gpg = None

    # Define the interface used to listen for messages
    if (args.interface):
        interface = args.interface
    elif (args.demo):
        interface = "lo"
    else:
        # Infer the interface based on the user's setup
        user_info = blocksatcli_config.read_cfg_file(args.cfg, args.cfg_dir)
        interface = blocksatcli_config.get_net_if(user_info)
    logger.info("Listening on interface: {}".format(interface))
    logger.info("Downloads will be saved at: {}".format(download_dir))

    # A passphrase is required for decryption (but not for signature
    # verification)
    if (not args.plaintext and not args.no_password):
        gpg.prompt_passphrase('GPG keyring password for decryption: ')

    # Listen continuously
    listen_loop = ApiListener()
    listen_loop.run(gpg,
                    download_dir,
                    args.sock_addr,
                    interface,
                    channel,
                    args.plaintext,
                    args.save_raw,
                    sender=args.sender,
                    stdout=args.stdout,
                    no_save=args.no_save,
                    echo=args.echo,
                    exec_cmd=args.exec,
                    gossip_opts=gossip_opts,
                    server_addr=server_addr,
                    tls_cert=args.tls_cert,
                    tls_key=args.tls_key,
                    region=args.region)


def bump(args):
    """Bump the bid of an API order"""
    server_addr = _get_server_addr(args.net, args.server)

    # Fetch the ApiOrder
    order = ApiOrder(server_addr, tls_cert=args.tls_cert, tls_key=args.tls_key)
    order.get(args.uuid, args.auth_token)

    # Bump bid
    try:
        res = order.bump(args.bid)
    except ValueError as e:
        logger.error(e)
        return

    # Print QR code
    qr = qrcode.QRCode()
    qr.add_data(res["lightning_invoice"]["payreq"])
    qr.print_ascii()

def
bump
(
args
)
:
server_addr
=
_get_server_addr
(
args
.
net
,
args
.
server
)
# Fetch the ApiOrder
order
=
ApiOrder
(
server_addr
,
tls_cert
=
args
.
tls_cert
,
tls_key
=
args
.
tls_key
)
order
.
get
(
args
.
uuid
,
args
.
auth_token
)
# Bump bid
try
:
res
=
order
.
bump
(
args
.
bid
)
except
ValueError
as
e
:
logger
.
error
(
e
)
return
# Print QR code
qr
=
qrcode
.
QRCode
(
)
qr
.
add_data
(
res
[
"lightning_invoice"
]
[
"payreq"
]
)
qr
.
print_ascii
(
)


def delete(args):
    """Cancel an API order"""
    server_addr = _get_server_addr(args.net, args.server)

    # Fetch the ApiOrder
    order = ApiOrder(server_addr, tls_cert=args.tls_cert, tls_key=args.tls_key)
    order.get(args.uuid, args.auth_token)

    # Delete it
    order.delete()

def
delete
(
args
)
:
server_addr
=
_get_server_addr
(
args
.
net
,
args
.
server
)
# Fetch the ApiOrder
order
=
ApiOrder
(
server_addr
,
tls_cert
=
args
.
tls_cert
,
tls_key
=
args
.
tls_key
)
order
.
get
(
args
.
uuid
,
args
.
auth_token
)
# Delete it
order
.
delete
(
)


def demo_rx(args):
    """Demo satellite receiver"""

    # Override some options based on the mutual exclusive --gossip and
    # --btc-src arguments, if present
    if (args.gossip):
        server_addr = server_map['gossip']
        channel = ApiChannel.GOSSIP.value
    elif (args.btc_src):
        server_addr = server_map['btc-src']
        channel = ApiChannel.BTC_SRC.value
    else:
        server_addr = _get_server_addr(args.net, args.server)
        channel = args.channel

    # Argument validation and special cases
    _warn_common_overrides(args)  # warn args overridden by --gossip/--btc-src

    # Open one socket for each interface:
    socks = list()
    for interface in args.interface:
        sock = net.UdpSock(args.dest, interface, mcast_rx=False)
        sock.set_mcast_tx_opts(args.ttl, args.dscp)
        socks.append(sock)

    rx = DemoRx(server_addr, socks, args.bitrate, args.event, channel,
                args.regions, args.tls_cert, args.tls_key)
    rx.run()


def subparser(subparsers):
    """Subparser for usb command"""
    def channel_number(value):
        ivalue = int(value)
        if ivalue < 0 or ivalue > 255:
            raise ArgumentTypeError(
                "Invalid API channel number {}. "
                "Choose number within [0, 256)".format(value))
        return ivalue

    p = subparsers.add_parser('api',
                              description="Blockstream Satellite API",
                              help='Blockstream Satellite API',
                              formatter_class=ArgumentDefaultsHelpFormatter)

    # Common API app arguments
    p.add_argument(
        '-g',
        '--gnupghome',
        default=".gnupg",
        help="GnuPG home directory, by default created inside the config "
        "directory specified via --cfg-dir option")
    server_addr = p.add_mutually_exclusive_group()
    server_addr.add_argument(
        '--net',
        choices=server_map.keys(),
        default=None,
        help="Choose between the Mainnet API server (main), the Testnet API \
        server (test), the receive-only server used for Lightning gossip \
        messages (gossip), or the receive-only server used for messages \
        carrying the Bitcoin source code (btc-src)")
    server_addr.add_argument('-s',
                             '--server',
                             default=server_map['main'],
                             help="Satellite API server address")
    p.add_argument(
        '--tls-cert',
        default=None,
        help="Certificate for client-side authentication with the API server")
    p.add_argument(
        '--tls-key',
        default=None,
        help="Private key for client-side authentication with the API server")
    p.set_defaults(func=print_help)

    subsubparsers = p.add_subparsers(title='subcommands',
                                     help='Target sub-command')

    # Config
    p1 = subsubparsers.add_parser(
        'config',
        aliases=['cfg'],
        description="Configure GPG keys",
        help="Configure GPG keys",
        formatter_class=ArgumentDefaultsHelpFormatter)
    p1.add_argument('-v',
                    '--verbose',
                    action='store_true',
                    default=False,
                    help="Verbose mode")
    p1.set_defaults(func=config)

    # Sender
    p2 = subsubparsers.add_parser(
        'send',
        aliases=['tx'],
        description=textwrap.dedent('''\

        Sends a file or a text message to the Satellite API for transmission
        via Blockstream Satellite. By default, runs the following sequence: 1)
        encapsulates the message into a structure containing the data checksum
        and the file name; 2) encrypts the entire structure using GnuPG; and 3)
        posts to the encrypted object to the API server for transmission.

        '''),
        help="Broadcast message through the Satellite API",
        formatter_class=ArgumentDefaultsHelpFormatter)
    msg_group = p2.add_mutually_exclusive_group()
    msg_group.add_argument('-f', '--file', help='File to send through the API')
    msg_group.add_argument('-m',
                           '--message',
                           help='Text message to send through the API')
    p2.add_argument('--bid',
                    default=None,
                    type=int,
                    help="Bid (in millisatoshis) for the message transmission")
    p2.add_argument(
        '-r',
        '--recipient',
        default=None,
        help="Public key fingerprint of the desired recipient. If not "
        "defined, the recipient will be automatically set to the first "
        "public key in the keyring")
    p2.add_argument(
        '--trust',
        default=False,
        action="store_true",
        help="Assume that the recipient\'s public key is fully trusted")
    p2.add_argument('--sign',
                    default=False,
                    action="store_true",
                    help="Sign message in addition to encrypting it")
    p2.add_argument(
        '--sign-key',
        default=None,
        help="Fingerprint of the private key to be used when signing the "
        "message. If not set, the default key from the keyring will be used")
    p2.add_argument(
        '--send-raw',
        default=False,
        action="store_true",
        help="Send the raw file or text message, i.e., without the data "
        "structure that includes the file name and checksum")
    p2.add_argument(
        '--plaintext',
        default=False,
        action="store_true",
        help="Send data in plaintext format, i.e., without encryption")
    p2.add_argument(
        '--fec',
        default=False,
        action="store_true",
        help="Send data with forward error correction (FEC) encoding")
    p2.add_argument(
        '--fec-overhead',
        default=0.1,
        type=float,
        help="Target ratio between the overhead FEC chunks and the original "
        "chunks. For example, 0.1 implies one overhead (redundant) chunk for "
        "every 10 original chunks")
    p2.add_argument(
        '--no-password',
        default=False,
        action="store_true",
        help="Whether to access the GPG keyring without a password")
    p2.add_argument(
        '--invoice-exec',
        help="Execute command with the Lightning invoice. Replaces the string "
        "\'{}\' with the Lightning bolt11 invoice string.")
    p2.add_argument(
        '--no-wait',
        default=False,
        action="store_true",
        help="Return immediately after submitting an API transmission order. "
        "Do not wait for the payment and transmission confirmations")
    p2.set_defaults(func=send)

    # Listen
    p3 = subsubparsers.add_parser(
        'listen',
        aliases=['rx'],
        description=textwrap.dedent('''\

        Receives data sent over the Blockstream Satellite network through the
        Satellite API. By default, assumes that the incoming messages are
        generated by the "send" command, which transmits encapsulated and
        encrypted messages. Thus, it first attempts to decrypt the data using a
        local GnuPG key. Then, on successful decryption, this application
        validates the integrity of the data. In the end, it saves the file into
        the download directory.

        '''),
        help="Listen to API messages",
        formatter_class=ArgumentDefaultsHelpFormatter)
    p3.add_argument(
        '--sock-addr',
        default=defs.api_dst_addr,
        help="Multicast UDP address (ip:port) used to listen for API data")
    intf_arg = p3.add_mutually_exclusive_group()
    intf_arg.add_argument('-i',
                          '--interface',
                          default=None,
                          help="Network interface that receives API data")
    intf_arg.add_argument(
        '-d',
        '--demo',
        action="store_true",
        default=False,
        help="Use the same interface as the demo-rx tool, i.e., the loopback "
        "interface")
    p3.add_argument(
        '-c',
        '--channel',
        default=ApiChannel.USER.value,
        type=channel_number,
        help="Listen to a specific API transmission channel. If set to 0, "
        "listen to all channels. By default, listen to user transmissions, "
        "which are sent on channel 1")
    p3.add_argument(
        '--save-raw',
        default=False,
        action="store_true",
        help="Save the raw decrypted data into the download directory while "
        "ignoring the existence of a data encapsulation structure")
    p3.add_argument(
        '--plaintext',
        default=False,
        action="store_true",
        help="Do not try to decrypt the incoming messages. Instead, assume "
        "that the incoming messages are in plaintext format and save them as "
        "individual files named with timestamps in the download directory. "
        "Note this option saves all incoming messages, including those "
        "broadcast by other users. In contrast, the default mode (without "
        "this option) only saves the messages that are successfully decrypted."
    )
    p3.add_argument(
        '--sender',
        default=None,
        help="Public key fingerprint of a target sender used to filter the "
        "incoming messages. When specified, the application processes only "
        "the messages that are digitally signed by the selected sender, "
        "including clearsigned messages.")
    p3.add_argument('--no-password',
                    default=False,
                    action="store_true",
                    help="Set to access GPG keyring without a password")
    p3.add_argument(
        '--echo',
        default=False,
        action='store_true',
        help="Print the contents of all incoming text messages to the "
        "console, as long as these messages are decodable in UTF-8")
    stdout_exec_arg_group = p3.add_mutually_exclusive_group()
    stdout_exec_arg_group.add_argument(
        '--stdout',
        default=False,
        action='store_true',
        help="Serialize the received data to stdout instead of saving on a "
        "file")
    stdout_exec_arg_group.add_argument(
        '--no-save',
        default=False,
        action='store_true',
        help="Do not save the files decoded from the received API messages")
    stdout_exec_arg_group.add_argument(
        '--exec',
        help="Execute arbitrary shell command for each downloaded file. "
        "Use the magic string \'{}\' to represent the file path within the "
        "command. For instance, run \"--exec \'cat {}\'\" to print every "
        "incoming file to stdout. For security, this option must be used in "
        "conjunction with the --sender option to limit the execution of the "
        "specified command to digitally signed messages from a specified "
        "sender only. See option --insecure for an alternative.")
    p3.add_argument(
        '--insecure',
        default=False,
        action="store_true",
        help="Run the --exec option while receiving messages from any sender. "
        "In this case, any successfully decrypted message (i.e., any message) "
        "encrypted using your public key will trigger the --exec command, "
        "which is considered insecure. Use at your own risk and avoid unsafe "
        "commands.")
    btc_src_gossip_arg_group1 = p3.add_mutually_exclusive_group()
    btc_src_gossip_arg_group1.add_argument(
        '--gossip',
        default=False,
        action="store_true",
        help="Configure the application to receive Lightning gossip snapshots "
        "and load them using the historian-cli application. This argument "
        "overrides the following options: 1) --plaintext (enabled); 2) "
        "--channel (set to {}); and 3) --server/--net (set to {})".format(
            ApiChannel.GOSSIP.value, server_map['gossip']))
    btc_src_gossip_arg_group1.add_argument(
        '--btc-src',
        default=False,
        action="store_true",
        help="Configure the application to receive API messages carrying the "
        "Bitcoin Satellite and Bitcoin Core source codes. This argument "
        "overrides the following options: 1) --plaintext (enabled); 2) "
        "--channel (set to {}); and 3) --server/--net (set to {})".format(
            ApiChannel.BTC_SRC.value, server_map['btc-src']))
    p3.add_argument(
        '--historian-path',
        default=None,
        help="Path to the historian-cli application. If not set, look for "
        "historian-cli globally")
    p3.add_argument(
        '--historian-destination',
        default=None,
        help="Destination for gossip snapshots, formatted as "
        "[nodeid]@[ipaddress]:[port]. If not set, historian-cli attempts to "
        "discover the destination automatically. This parameter is provided "
        "as a positional argument of command \'historian-cli snapshot load\', "
        "which is called for each downloaded file in gossip mode (i.e., when "
        "argument --gossip is set).")
    p3.add_argument('-r',
                    '--region',
                    choices=range(0, 6),
                    type=int,
                    help="Coverage region for Rx confirmations")
    p3.set_defaults(func=listen)

    # Bump
    p4 = subsubparsers.add_parser(
        'bump',
        description="Bump bid of an API message order",
        help="Bump bid of an API message order",
        formatter_class=ArgumentDefaultsHelpFormatter)
    p4.add_argument(
        '--bid',
        default=None,
        type=int,
        help="New bid (in millisatoshis) for the message transmission")
    p4.add_argument('-u',
                    '--uuid',
                    default=None,
                    help="API order's universally unique identifier (UUID)")
    p4.add_argument('-a',
                    '--auth-token',
                    default=None,
                    help="API order's authentication token")
    p4.set_defaults(func=bump)

    # Delete
    p5 = subsubparsers.add_parser(
        'delete',
        aliases=['del'],
        description="Delete API message order",
        help="Delete API message order",
        formatter_class=ArgumentDefaultsHelpFormatter)
    p5.add_argument('-u',
                    '--uuid',
                    default=None,
                    help="API order's universally unique identifier (UUID)")
    p5.add_argument('-a',
                    '--auth-token',
                    default=None,
                    help="API order's authentication token")
    p5.set_defaults(func=delete)

    # Demo receiver
    p6 = subsubparsers.add_parser(
        'demo-rx',
        description=textwrap.dedent('''\

        Demo Blockstream Satellite Receiver, used to test the API data listener
        application without an actual satellite receiver. The satellite
        receiver receives UDP packets sent over satellite and relays these
        packets to a host PC, where the API data listener application runs.
        This application, in turn, produces equivalent UDP packets, just like
        the satellite receiver would. The difference is that it fetches the
        data through the internet, rather than receiving via satellite.

        '''),
        help='Run demo satellite receiver',
        formatter_class=ArgumentDefaultsHelpFormatter)
    p6.add_argument(
        '-d',
        '--dest',
        default=defs.api_dst_addr,
        help="Destination address (ip:port) to which API data will be sent")
    p6.add_argument(
        '-i',
        '--interface',
        nargs="+",
        help="Network interface(s) over which to send the API data. If \
        multiple interfaces are provided, the same packets are sent over all \
        interfaces.",
        default=["lo"])
    p6.add_argument('-c',
                    '--channel',
                    default=ApiChannel.USER.value,
                    type=channel_number,
                    help="Target API transmission channel")
    p6.add_argument('-r',
                    '--regions',
                    nargs="+",
                    choices=range(0, 6),
                    type=int,
                    help="Coverage region for Tx confirmations")
    p6.add_argument('--ttl',
                    type=int,
                    default=1,
                    help="Time-to-live to set on multicast packets")
    p6.add_argument(
        '--dscp',
        type=int,
        default=0,
        help="Differentiated services code point (DSCP) to set on the output \
        multicast IP packets")
    p6.add_argument(
        '--bitrate',
        type=float,
        default=1000,
        help="Maximum bit rate in kbps of the output packet stream")
    p6.add_argument('-e',
                    '--event',
                    choices=["transmitting", "sent"],
                    default="sent",
                    help='SSE event that should trigger packet transmissions')
    btc_src_gossip_arg_group2 = p6.add_mutually_exclusive_group()
    btc_src_gossip_arg_group2.add_argument(
        '--gossip',
        default=False,
        action="store_true",
        help="Configure the application to fetch and relay Lightning gossip "
        "messages. This argument overrides option --channel (set to {}) and "
        "options --server/--net (set to {})".format(ApiChannel.GOSSIP.value,
                                                    server_map['gossip']))
    btc_src_gossip_arg_group2.add_argument(
        '--btc-src',
        default=False,
        action="store_true",
        help="Configure the application to fetch and relay messages carrying "
        "the Bitcoin Satellite and Bitcoin Core source codes. This argument "
        "overrides option --channel (set to {}) and options --server/--net "
        "(set to {})".format(ApiChannel.BTC_SRC.value, server_map['btc-src']))
    p6.set_defaults(func=demo_rx)

    return p

"directory specified via --cfg-dir option"

"Broadcast message through the Satellite API"

"Bid (in millisatoshis) for the message transmission"

"Sign message in addition to encrypting it"

"structure that includes the file name and checksum"

"Send data in plaintext format, i.e., without encryption"

"Send data with forward error correction (FEC) encoding"

"Whether to access the GPG keyring without a password"

"\'{}\' with the Lightning bolt11 invoice string."

"Network interface that receives API data"

"ignoring the existence of a data encapsulation structure"

"Set to access GPG keyring without a password"

"Print the contents of all incoming text messages to the "

"sender only. See option --insecure for an alternative."

"--channel (set to {}); and 3) --server/--net (set to {})"

"Destination for gossip snapshots, formatted as "

"New bid (in millisatoshis) for the message transmission"

"API order's universally unique identifier (UUID)"

"Time-to-live to set on multicast packets"

"Maximum bit rate in kbps of the output packet stream"

'SSE event that should trigger packet transmissions'


Re - create argparse s help menu for the api command

def print_help(args):
    """Re-create argparse's help menu for the api command"""
    parser = ArgumentParser()
    subparsers = parser.add_subparsers(title='', help='')
    parser = subparser(subparsers)
    print(parser.format_help())

def print_help ( args ) : parser = ArgumentParser ( ) subparsers = parser . add_subparsers ( title = '' , help = '' ) parser = subparser ( subparsers ) print ( parser . format_help ( ) )


Compute the number of bytes sent over - the - air ( OTA ) for an API message

def calc_ota_msg_len(msg_len):
    """Compute the number of bytes sent over-the-air (OTA) for an API message

    The API message is carried by Blocksat Packets, sent in the payload of UDP
    datagrams over IPv4, with a layer-2 MTU of 1500 bytes, and, ultimately,
    transported over MPE. If the message size is such that the UDP/IPv4 packet
    exceeds the layer-2 MTU, fragmentation is not handled at the IP level but
    instead at application layer, i.e., at the Blocksat Packet protocol level.

    Args:
        msg_len : Length of the API message to be transmitted

    """
    # Is it going to be fragmented?
    n_frags = ceil(msg_len / MAX_PAYLOAD)

    # Including all fragments, the total Blocksat + UDP + IPv4 overhead is:
    total_overhead = (UDP_IP_HEADER + HEADER_LEN) * n_frags

    # Total overhead at MPE layer:
    mpe_header = 16
    total_mpe_overhead = mpe_header * n_frags

    return total_mpe_overhead + total_overhead + msg_len


def pack(self):
        """Form Blocksat Packet
        """
        # Assert the "more fragments" (MF) bit if this isn't the last fragment
        octet_0 = API_TYPE_MORE_FRAG if self.more_frags else API_TYPE_LAST_FRAG
        header = struct.pack(HEADER_FORMAT, octet_0, self.chan_num,
                             self.frag_num, self.seq_num)
        return header + self.payload


def unpack(self, udp_payload):
        """Unpack Blocksat Packet from UDP payload

        Args:
            udp_payload : UDP payload received via socket (bytes)

        Returns:
            Tuple with the Blocksat Packet's payload (bytes) and sequence
            number.

        """
        assert (isinstance(udp_payload, bytes))
        assert (len(udp_payload) >= HEADER_LEN)

        # Separate header and payload
        header = udp_payload[:HEADER_LEN]
        self.payload = udp_payload[HEADER_LEN:]

        # Parse header
        octet_0, self.chan_num, self.frag_num, self.seq_num = struct.unpack(
            HEADER_FORMAT, header)

        # Sanity check
        assert (ord(octet_0) & 1), "Not an API packet"

        # Are there more fragments coming?
        self.more_frags = bool(ord(octet_0) & ord(b'\x80'))

        logger.debug("BlocksatPkt: Seq Num: {} / Frag Num: {} / MF: {}".format(
            self.seq_num, self.frag_num, self.more_frags))


def __init__(self, timeout=7200):
        """BlocksatPktHandler Constructor

        Args:
            timeout : Timeout in seconds to remove old pending fragments.

        """
        self.frag_map = {}
        self.timeout = timeout

def __init__ ( self , timeout = 7200 ) : self . frag_map = { } self . timeout = timeout


def _check_gaps(self, seq_num):
        """Check if there is any fragment number gap

        Returns:
            (bool) True when there are no gaps (everything is OK).

        """
        frag_idxs = sorted(self.frag_map[seq_num]['frags'].keys())
        for i, x in enumerate(frag_idxs):
            if (i == 0 and x != 0):
                if (x > 1):
                    logger.warning("First {:d} fragments were lost".format(x))
                else:
                    logger.warning("First fragment was lost")
                return False
            elif (i > 0 and x - frag_idxs[i - 1] != 1):
                logger.warning("Gap between fragment {:d} and fragment "
                               "{:d}".format(frag_idxs[i - 1], x))
                return False
        return True


def _check_ready(self, seq_num):
        """Check if the collection of Blocksat Packets is ready to be decoded

        Returns:
            (bool) True when ready to be decoded.

        """
        # The message can only be ready when the last fragment is available:
        i_last_frag = self.frag_map[seq_num]['last_frag']
        if (i_last_frag is None):
            return False

        # Because packets can be processed out of order, check that all packets
        # before the last fragment are also available:
        frags = self.frag_map[seq_num]['frags'].keys()
        return all([i in frags for i in range(i_last_frag)])


def _concat_pkt(self, pkt):
        """Concatenate a single BlocksatPkt within a concatenation cache"""
        seq_num = pkt.seq_num

        # If it's an out-of-order packet, re-compute the concatenated
        # version. Otherwise, just append directly to the cache.
        if (self.frag_map[seq_num]['high_frag'] is not None
                and pkt.frag_num < self.frag_map[seq_num]['high_frag']):

            concat_msg = bytearray()
            for i_frag, frag in sorted(
                    self.frag_map[seq_num]['frags'].items()):
                concat_msg += frag.payload

            self.frag_map[seq_num]['concat'] = concat_msg
        else:
            self.frag_map[seq_num]['concat'] += pkt.payload


def append(self, pkt):
        """Append incoming BlocksatPkt

        Append the incoming Blocksat packet to the map of packets (fragments)
        pertaining to the same API message. Once the last fragment of a
        sequence comes, let the caller know that the API message is ready to be
        decoded.

        Args:
            pkt : Incoming BlocksatPkt

        Returns:
            (bool) True if the underlying ApiMsg is ready to be extracted from
            the corresponding collection of packets.

        """
        assert (isinstance(pkt, BlocksatPkt))

        if (pkt.seq_num not in self.frag_map):
            self.frag_map[pkt.seq_num] = {
                'high_frag': None,
                'last_frag': None,
                'concat': bytearray()
            }
            self.frag_map[pkt.seq_num]['frags'] = {}

        # Do not process a repeated fragment
        if (pkt.frag_num in self.frag_map[pkt.seq_num]['frags']):
            logger.debug("BlocksatPktHandler: fragment {} has already "
                         "been received".format(pkt.frag_num))
            # Check if the repeated fragment actually has the same contents
            pre_existing_pkt = self.frag_map[pkt.seq_num]['frags'][
                pkt.frag_num]
            if (pkt.payload != pre_existing_pkt.payload):
                logger.warning(
                    "Got the same fragment twice but with different contents "
                    "(frag_num: {}, seq_num: {})".format(
                        pkt.frag_num, pkt.seq_num))
            return self._check_ready(pkt.seq_num)

        self.frag_map[pkt.seq_num]['frags'][pkt.frag_num] = pkt

        # Timestamp the last fragment reception of this sequence number. Use
        # this timestamp to clean old fragments that were never decoded.
        self.frag_map[pkt.seq_num]['t_last'] = time.time()

        # Concatenate payload by payload instead of waiting to concatenate
        # everything in the end when the message is ready.
        self._concat_pkt(pkt)

        # Track the highest fragment number received so far. This information
        # is used to identify whether the concatenation cache needs to be
        # recomputed for out-of-order packets (see _concat_pkt()).
        if (self.frag_map[pkt.seq_num]['high_frag'] is None
                or pkt.frag_num > self.frag_map[pkt.seq_num]['high_frag']):
            self.frag_map[pkt.seq_num]['high_frag'] = pkt.frag_num

        # Track the last fragment of the sequence. This information is used to
        # more quickly verify whether the message is ready (see
        # _check_ready()).
        if (not pkt.more_frags):
            self.frag_map[pkt.seq_num]['last_frag'] = pkt.frag_num

        logger.debug("BlocksatPktHandler: Append fragment {}, "
                     "Seq Num {}".format(pkt.frag_num, pkt.seq_num))

        return self._check_ready(pkt.seq_num)


def get_frags(self, seq_num):
        """Get the Blocksat Packets sorted by fragment number"""
        return [x[1] for x in sorted(self.frag_map[seq_num]['frags'].items())]

def get_frags ( self , seq_num ) : return [ x [ 1 ] for x in sorted ( self . frag_map [ seq_num ] [ 'frags' ] . items ( ) ) ]


def get_n_frags(self, seq_num):
        """Return the number of fragments corresponding to a sequence number"""
        return len(self.frag_map[seq_num]['frags'].keys())

def get_n_frags ( self , seq_num ) : return len ( self . frag_map [ seq_num ] [ 'frags' ] . keys ( ) )


def concat(self, seq_num, force=False):
        """Concatenate all Blocksat Packet payloads composing an API message

        Args:
            seq_num : API message sequence number
            force   : Force concatenation even if there are fragment gaps

        Returns:
            Bytes array with the concatenated payloads

        """
        assert (seq_num in self.frag_map)

        # The caller should call this function only when it's known that the
        # message can be decoded
        if (not force and not self._check_gaps(seq_num)):
            raise RuntimeError("Gap found between message fragments")

        if (not force and not self._check_ready(seq_num)):
            raise RuntimeError("Tried to decode while fragments are missing")

        logger.debug("BlocksatPktHandler: Concatenated message with {} bytes "
                     "(force: {})".format(
                         len(self.frag_map[seq_num]['concat']), force))

        # Take the concatenated message directly from the cache
        return bytes(self.frag_map[seq_num]['concat'])


Split data array into Blocksat Packet ( s )

def split(self, data, seq_num, chan_num):
        """Split data array into Blocksat Packet(s)

        An API message may be sent over multiple packet in case its length
        exceeds the maximum UDP payload. This function splits the message into
        packets and saves all the generated Blocksat Packets into the internal
        fragment map.

        Args:
            data     : Bytes object containing the API message data
            seq_num  : API Tx sequence number (`tx_seq_num` field)
            chan_num : API channel number

        """
        assert (isinstance(data, bytes))
        n_frags = ceil(len(data) / MAX_PAYLOAD)

        logger.debug("BlocksatPktHandler: Message size: {:d} bytes\t"
                     "Fragments: {:d}".format(len(data), n_frags))

        for i_frag in range(n_frags):
            # Is this the last_fragment?
            more_frags = (i_frag + 1) < n_frags

            # Byte range of the data to send on this Blocksat packet
            s_byte = i_frag * MAX_PAYLOAD  # starting byte
            e_byte = (i_frag + 1) * MAX_PAYLOAD  # ending byte

            # Packetize
            pkt = BlocksatPkt(seq_num, i_frag, chan_num, more_frags,
                              data[s_byte:e_byte])

            # Add to fragment map
            self.append(pkt)


Throw away old ( timed - out ) pending fragments

def clean(self):
        """Throw away old (timed-out) pending fragments
        """
        # Find the timed-out messages
        timed_out = []
        t_now = time.time()
        for seq_num in self.frag_map:
            if (t_now - self.frag_map[seq_num]['t_last'] > self.timeout):
                timed_out.append(seq_num)

        # Delete the timed-out messages
        for seq_num in timed_out:
            logger.debug("BlocksatPktHandler: Delete Seq Num {} from fragment "
                         "map".format(seq_num))
            del self.frag_map[seq_num]


def __init__(self, overhead=0.1):
        """Constructor

        Args:
            overhead : Percentage of the FEC chunks to add as overhead
                       (rounded up).
        """
        self.overhead = overhead

def __init__ ( self , overhead = 0.1 ) : self . overhead = overhead


def _encode_obj(self, data):
        """Encode a single FEC object

        Args:
            data : Data to encode (bytes array)

        Note:
            Unlike method encode(), this method must take a data object that
            fits within 256 chunks with overhead so that it fits into a single
            FEC object. Furthermore, while encode() returns FEC packets,
            this method returns FEC chunks.

        Returns:
            List of FEC-encoded chunks.

        """
        assert (isinstance(data, bytes))

        n_chunks = ceil(len(data) / CHUNK_SIZE)
        n_overhead_chunks = ceil(self.overhead * n_chunks)
        n_fec_chunks = n_chunks + n_overhead_chunks

        assert (n_fec_chunks <= MAX_FEC_CHUNKS)
        logger.debug("Original Chunks: {} / "
                     "Overhead Chunks: {} / "
                     "Total: {}".format(n_chunks, n_overhead_chunks,
                                        n_fec_chunks))

        # Split the given data array into chunks
        chunks = []
        for i_chunk in range(n_chunks):
            # Byte range of the next chunk:
            s_byte = i_chunk * CHUNK_SIZE  # starting byte
            e_byte = (i_chunk + 1) * CHUNK_SIZE  # ending byte
            chunk = data[s_byte:e_byte]

            # The last chunk may need zero-padding
            if (i_chunk + 1 == n_chunks and len(chunk) < CHUNK_SIZE):
                chunk += bytes(CHUNK_SIZE - len(chunk))

            assert (len(chunk) == CHUNK_SIZE)
            chunks.append(chunk)

        # Generate the corresponding FEC chunks
        encoder = zfec.Encoder(n_chunks, n_fec_chunks)
        return encoder.encode(chunks)


def encode(self, data):
        """Encode message into FEC packets encompassing multiple FEC objects

        Generate the FEC packets (chunk + metadata) for the given message. If
        the message exceeds the capacity of one FEC object (up to 256 chunks),
        generate multiple FEC objects.

        Args:
            data : Data to encode (bytes array)

        Returns:
            Bytes array with all FEC packets serially (concatenated).

        """
        assert (isinstance(data, bytes))

        # The FEC object should contain up to 256 chunks, including the
        # original (systematic) and the overhead chunks. The original data
        # object (to be encoded) should occupy up to "256/(1 + overhead)"
        # chunks, whereas the overhead chunks should occupy "(overhead *
        # 256)/(1 + overhead)" chunks. Also, the overhead cannot be greater
        # than 255, otherwise the original data has zero chunks per object.
        assert(self.overhead <= MAX_OVERHEAD), \
            "FEC overhead exceeds the maximum of {}".format(MAX_OVERHEAD)

        max_obj_size = floor(MAX_FEC_CHUNKS / (1 + self.overhead)) * CHUNK_SIZE
        n_fec_objects = ceil(len(data) / max_obj_size)

        logger.debug("Message Size: {} / FEC Objects: {}".format(
            len(data), n_fec_objects))

        # Generate the FEC packets from (potentially) multiple FEC objects
        fec_pkts = []
        for i_obj in range(n_fec_objects):
            s_byte = i_obj * max_obj_size  # starting byte
            e_byte = (i_obj + 1) * max_obj_size  # ending byte
            fec_object = data[s_byte:e_byte]

            logger.debug("FEC Object: {}".format(i_obj))

            fec_chunks = self._encode_obj(fec_object)

            # FEC packets
            for i_chunk, chunk in enumerate(fec_chunks):
                metadata = struct.pack(HEADER_FORMAT, i_obj, n_fec_objects,
                                       i_chunk, len(fec_object))
                fec_pkts.append(metadata + chunk)

        # Concatenate all FEC packets to form a single encoded data
        # array. Concatenate them in random order so that the chunks from the
        # same object are not sent consecutively. This strategy is useful to
        # avoid error bursts.
        encoded_data = bytearray()
        random.shuffle(fec_pkts)
        for fec_pkt in fec_pkts:
            encoded_data += fec_pkt

        assert (len(encoded_data) % PKT_SIZE == 0)

        return bytes(encoded_data)


def _decode_obj(self, obj_len, chunks, chunk_ids):
        """Decode a single FEC object

        Args:
            obj_len   : Length of the original object encoded by the FEC
                        object.
            chunks    : List of FEC chunks.
            chunk_ids : List of ids associated with the list of FEC chunks.

        Note:
            Unlike method decode(), this method processes a single encoded FEC
            object.

        Returns:
            (bytearray) with the decoded (original) data object.

        """
        # The original (uncoded) object must fit within the maximum number of
        # FEC chunks. Most of the time `obj_len` will be less than the
        # maximum. It only hits the maximum if the FEC overhead is set to zero.
        assert (obj_len <= (MAX_FEC_CHUNKS * CHUNK_SIZE))

        # Number of FEC chunks required to decode the original message:
        n_chunks = ceil(obj_len / CHUNK_SIZE)

        # FEC decoder
        decoder = zfec.Decoder(n_chunks, MAX_FEC_CHUNKS)
        # NOTE: The hard-coded "MAX_FEC_CHUNKS" represents the maximum number
        # of chunks that can be generated. The receiver does not know how many
        # chunks the sender really generated. Nevertheless, it does not need to
        # know, as long as it receives at least n_chunks (any combination of
        # n_chunks). The explanation for this requirement is that the FEC
        # scheme is a maximum distance separable (MDS) erasure code.

        # Decode using the minimum required number of FEC chunks
        decoded_chunks = decoder.decode(chunks[:n_chunks],
                                        chunk_ids[:n_chunks])

        # Concatenate the decoded chunks to form the original object
        decoded_obj = bytearray()
        for chunk in decoded_chunks:
            assert (len(chunk) == CHUNK_SIZE)
            decoded_obj += chunk

        # Remove any zero-padding that may have been applied to the last chunk:
        return decoded_obj[:obj_len]


Check if the FEC - encoded data is decodable

def _is_decodable(self, data):
        """Check if the FEC-encoded data is decodable

        Args:
            data : FEC-encoded data, a bytes array with multiple FEC packets
                   serially.

        Note: This is a faster processing tailored specifically to validate the
            given FEC-encoded data. Unlike method decode(), it does not make
            any expensive memory copying. The rationale is that the data will
            not be ready to be decoded most of the time. Hence, it is better to
            check if decodable separately than to try and decode it in one go.

        """

        # The encoded data must contain an integer number of FEC packets,
        # although it may not contain the full FEC-encoded object(s), given
        # that some parts of it may have been lost.
        if (len(data) % PKT_SIZE != 0):
            logger.debug("Not a properly formatted FEC-encoded object")
            return False

        # Process each packet and create a map of FEC objects and chunks
        n_fec_pkts = len(data) // PKT_SIZE
        fec_map = {}
        n_fec_objects = None
        for i_fec_pkt in range(n_fec_pkts):
            # Byte range of the next header (don't process the payload here):
            s_byte = i_fec_pkt * PKT_SIZE  # starting byte
            e_byte = s_byte + HEADER_LEN  # ending byte

            # Unpack the metadata from the FEC header
            metadata = struct.unpack(HEADER_FORMAT, data[s_byte:e_byte])
            obj_id = metadata[0]
            chunk_id = metadata[2]
            obj_len = metadata[3]

            # Check if the chunk id is valid
            if (chunk_id >= MAX_FEC_CHUNKS):
                logger.debug("Invalid chunk id - likely not FEC-encoded")
                return False

            # All packets of a FEC object should bring the same message length
            if (obj_id not in fec_map):
                fec_map[obj_id] = {'len': obj_len, 'n_chunks': 0}
                # NOTE: keep the count of chunks here (not the actual chunks).
            elif (obj_len != fec_map[obj_id]['len']):
                logger.debug("Inconsistent message length on FEC packets - "
                             "likely not FEC-encoded")
                return False

            # All FEC packets should bring the same metadata information
            # regarding the number of FEC objects
            if (n_fec_objects is None):
                n_fec_objects = metadata[1]
            elif (n_fec_objects != metadata[1]):
                logger.debug("Inconsistent number of FEC objects - "
                             "likely not FEC-encoded")
                return False

            # Increment the count of FEC chunks
            fec_map[obj_id]['n_chunks'] += 1

        # The decoder needs all the FEC objects
        if (n_fec_objects > len(fec_map)):
            logger.debug("Insufficient number of FEC objects")
            return False

        # All FEC objects should contain enough FEC chunks
        ready = n_fec_objects * [False]
        for i_obj in range(n_fec_objects):
            n_chunks = ceil(fec_map[i_obj]['len'] / CHUNK_SIZE)
            ready[i_obj] = fec_map[i_obj]['n_chunks'] >= n_chunks

        if (not all(ready)):
            logger.debug("Insufficient number of FEC chunks")
            return False

        logger.debug("Object decodable")

        return True


def decode(self, data):
        """Decode a sequence of FEC packets spanning multiple FEC objects

        Args:
            data : FEC-encoded data, a bytes array with multiple FEC packets
                   serially.

        Returns:
            False if decoding fails. Otherwise, a bytes array with the decoded
            data (i.e., the original message).

        """

        # Check if the given FEC-encoded can decoded before anything to avoid
        # the more expensive message decoding that follows.
        if (not self._is_decodable(data)):
            return False

        # Process each packet and create a map of FEC objects and chunks
        n_fec_pkts = len(data) // PKT_SIZE
        fec_map = {}
        n_fec_objects = None
        for i_fec_pkt in range(n_fec_pkts):
            # Byte range of the next FEC packet:
            s_byte = i_fec_pkt * PKT_SIZE  # starting byte
            e_byte = (i_fec_pkt + 1) * PKT_SIZE  # ending byte
            fec_pkt = data[s_byte:e_byte]

            # Unpack the metadata from the FEC header
            obj_id, n_fec_objects, chunk_id, obj_len = struct.unpack(
                HEADER_FORMAT, fec_pkt[:HEADER_LEN])

            # Save the FEC object length and the FEC chunk
            if (obj_id not in fec_map):
                fec_map[obj_id] = {'len': obj_len, 'chunks': {}}

            fec_map[obj_id]['chunks'][chunk_id] = fec_pkt[HEADER_LEN:]

        logger.debug("Processed chunks: {} / Processed objects: {}".format(
            n_fec_pkts, len(fec_map)))

        # Decode multiple objects and concatenate the decoded data
        decoded_data = bytearray()
        for i_obj in range(n_fec_objects):
            decoded_data += self._decode_obj(
                fec_map[i_obj]['len'], list(fec_map[i_obj]['chunks'].values()),
                list(fec_map[i_obj]['chunks'].keys()))

        return bytes(decoded_data)