Ejemplo n.º 1
0
    def upgrade_bw_accounting_db_8to9(self):
        """
        Upgrade the database with bandwidth accounting information from 8 to 9.
        Specifically, this upgrade wipes all transactions and addresses an issue where payouts with the wrong amount
        were made. Also see https://github.com/Tribler/tribler/issues/5789.
        """
        to_version = 9

        database_path = self.session.config.get_state_dir(
        ) / 'sqlite' / 'bandwidth.db'
        if not database_path.exists() or get_db_version(database_path) >= 9:
            return  # No need to update if the database does not exist or is already updated
        db = BandwidthDatabase(database_path,
                               self.session.trustchain_keypair.key.pk)

        # Wipe all transactions and bandwidth history
        with db_session:
            delete(tx for tx in db.BandwidthTransaction)
            delete(item for item in db.BandwidthHistory)

            # Update db version
            db_version = db.MiscData.get(name="db_version")
            db_version.value = str(to_version)

        db.shutdown()
Ejemplo n.º 2
0
    def __init__(self, *args, **kwargs) -> None:
        """
        Initialize the community.
        :param persistence: The database that stores transactions, will be created if not provided.
        :param database_path: The path at which the database will be created. Defaults to the current working directory.
        """
        self.settings = kwargs.pop('settings', BandwidthAccountingSettings())
        self.database = kwargs.pop('database', None)
        self.database_path = Path(kwargs.pop('database_path', ''))
        self.random = Random()

        super().__init__(*args, **kwargs)

        self.request_cache = RequestCache()
        self.my_pk = self.my_peer.public_key.key_to_bin()

        if not self.database:
            self.database = BandwidthDatabase(self.database_path, self.my_pk)

        self.add_message_handler(BandwidthTransactionPayload,
                                 self.received_transaction)
        self.add_message_handler(BandwidthTransactionQueryPayload,
                                 self.received_query)

        self.register_task("query_peers",
                           self.query_random_peer,
                           interval=self.settings.outgoing_query_interval)

        self.logger.info(
            "Started bandwidth accounting community with public key %s",
            hexlify(self.my_pk))
Ejemplo n.º 3
0
async def test_upgrade_bw_accounting_db_8to9(upgrader, session):
    old_db_sample = TESTS_DATA_DIR / 'upgrade_databases' / 'bandwidth_v8.db'
    database_path = session.config.get_state_dir() / 'sqlite' / 'bandwidth.db'
    shutil.copyfile(old_db_sample, database_path)

    upgrader.upgrade_bw_accounting_db_8to9()
    db = BandwidthDatabase(database_path, session.trustchain_keypair.key.pk)
    with db_session:
        assert not list(select(tx for tx in db.BandwidthTransaction))
        assert not list(select(item for item in db.BandwidthHistory))
        assert int(db.MiscData.get(name="db_version").value) == 9
    db.shutdown()
Ejemplo n.º 4
0
    def get_kwargs(self, session):
        settings = BandwidthAccountingSettings()
        settings.outgoing_query_interval = 5
        database = BandwidthDatabase(
            session.config.get_state_dir() / "sqlite" / "bandwidth.db",
            session.trustchain_keypair.pub().key_to_bin(),
            store_all_transactions=True)

        return {"database": database, "settings": settings, "max_peers": -1}
Ejemplo n.º 5
0
class BandwidthAccountingCommunity(Community):
    """
    Community around bandwidth accounting and payouts.
    """
    community_id = unhexlify('79b25f2867739261780faefede8f25038de9975d')
    DB_NAME = 'bandwidth'
    version = b'\x02'

    def __init__(self, *args, **kwargs) -> None:
        """
        Initialize the community.
        :param persistence: The database that stores transactions, will be created if not provided.
        :param database_path: The path at which the database will be created. Defaults to the current working directory.
        """
        self.settings = kwargs.pop('settings', BandwidthAccountingSettings())
        self.database = kwargs.pop('database', None)
        self.database_path = Path(kwargs.pop('database_path', ''))
        self.random = Random()

        super().__init__(*args, **kwargs)

        self.request_cache = RequestCache()
        self.my_pk = self.my_peer.public_key.key_to_bin()

        if not self.database:
            self.database = BandwidthDatabase(self.database_path, self.my_pk)

        self.add_message_handler(BandwidthTransactionPayload,
                                 self.received_transaction)
        self.add_message_handler(BandwidthTransactionQueryPayload,
                                 self.received_query)

        self.register_task("query_peers",
                           self.query_random_peer,
                           interval=self.settings.outgoing_query_interval)

        self.logger.info(
            "Started bandwidth accounting community with public key %s",
            hexlify(self.my_pk))

    def construct_signed_transaction(self, peer: Peer,
                                     amount: int) -> BandwidthTransactionData:
        """
        Construct a new signed bandwidth transaction.
        :param peer: The counterparty of the transaction.
        :param amount: The amount of bytes to payout.
        :return A signed BandwidthTransaction.
        """
        peer_pk = peer.public_key.key_to_bin()
        latest_tx = self.database.get_latest_transaction(self.my_pk, peer_pk)
        total_amount = latest_tx.amount + amount if latest_tx else amount
        next_seq_num = latest_tx.sequence_number + 1 if latest_tx else 1
        tx = BandwidthTransactionData(next_seq_num, self.my_pk, peer_pk,
                                      EMPTY_SIGNATURE, EMPTY_SIGNATURE,
                                      total_amount)
        tx.sign(self.my_peer.key, as_a=True)
        return tx

    def do_payout(self, peer: Peer, amount: int) -> Future:
        """
        Conduct a payout with a given amount of bytes to a peer.
        :param peer: The counterparty of the payout.
        :param amount: The amount of bytes to payout.
        :return A Future that fires when the counterparty has acknowledged the payout.
        """
        tx = self.construct_signed_transaction(peer, amount)
        with db_session:
            self.database.BandwidthTransaction.insert(tx)
        cache = self.request_cache.add(BandwidthTransactionSignCache(self, tx))
        self.send_transaction(tx, peer.address, cache.number)

        return cache.future

    def send_transaction(self, transaction: BandwidthTransactionData,
                         address: Address, request_id: int) -> None:
        """
        Send a provided transaction to another party.
        :param transaction: The BandwidthTransaction to send to the other party.
        :param peer: The IP address and port of the peer.
        :param request_id: The identifier of the message, is usually provided by a request cache.
        """
        payload = BandwidthTransactionPayload.from_transaction(
            transaction, request_id)
        packet = self._ez_pack(self._prefix, 1, [payload], False)
        self.endpoint.send(address, packet)

    def received_transaction(self, source_address: Address,
                             data: bytes) -> None:
        """
        Callback when we receive a transaction from another peer.
        :param source_address: The network address of the peer that has sent us the transaction.
        :param data: The serialized, raw data in the packet.
        """
        payload = self._ez_unpack_noauth(BandwidthTransactionPayload,
                                         data,
                                         global_time=False)
        tx = BandwidthTransactionData.from_payload(payload)

        if not tx.is_valid():
            self.logger.info("Transaction %s not valid, ignoring it", tx)
            return

        if payload.public_key_a == self.my_pk or payload.public_key_b == self.my_pk:
            # This transaction involves this peer.
            latest_tx = self.database.get_latest_transaction(
                tx.public_key_a, tx.public_key_b)
            if payload.public_key_b == self.my_peer.public_key.key_to_bin():
                from_peer = Peer(payload.public_key_a, source_address)
                if latest_tx:
                    # Check if the amount in the received transaction is higher than the amount of the latest one
                    # in the database.
                    if payload.amount > latest_tx.amount:
                        # Sign it, store it, and send it back
                        tx.sign(self.my_peer.key, as_a=False)
                        self.database.BandwidthTransaction.insert(tx)
                        self.send_transaction(tx, from_peer.address,
                                              payload.request_id)
                    else:
                        self.logger.info(
                            "Received older bandwidth transaction from peer %s:%d - "
                            "sending back the latest one", *from_peer.address)
                        self.send_transaction(latest_tx, from_peer.address,
                                              payload.request_id)
                else:
                    # This transaction is the first one with party A. Sign it, store it, and send it back.
                    tx.sign(self.my_peer.key, as_a=False)
                    self.database.BandwidthTransaction.insert(tx)
                    from_peer = Peer(payload.public_key_a, source_address)
                    self.send_transaction(tx, from_peer.address,
                                          payload.request_id)
            elif payload.public_key_a == self.my_peer.public_key.key_to_bin():
                # It seems that we initiated this transaction. Check if we are waiting for it.
                cache = self.request_cache.get("bandwidth-tx-sign",
                                               payload.request_id)
                if not cache:
                    self.logger.info(
                        "Received bandwidth transaction %s without associated cache entry, ignoring it",
                        tx)
                    return

                if not latest_tx or (latest_tx
                                     and latest_tx.amount >= tx.amount):
                    self.database.BandwidthTransaction.insert(tx)

                cache.future.set_result(tx)
        else:
            # This transaction involves two unknown peers. We can add it to our database.
            self.database.BandwidthTransaction.insert(tx)

    def query_random_peer(self) -> None:
        """
        Query a random peer neighbouring peer and ask their bandwidth transactions.
        """
        peers = list(self.network.verified_peers)
        if peers:
            random_peer = self.random.choice(peers)
            self.query_transactions(random_peer)

    def query_transactions(self, peer: Peer) -> None:
        """
        Query the transactions of a specific peer and ask for their bandwidth transactions.
        :param peer: The peer to send the query to.
        """
        self.logger.info("Querying the transactions of peer %s:%d",
                         *peer.address)
        payload = BandwidthTransactionQueryPayload()
        packet = self._ez_pack(self._prefix, 2, [payload], False)
        self.endpoint.send(peer.address, packet)

    def received_query(self, source_address: Address, data: bytes) -> None:
        """
        We received a query from another peer.
        :param source_address: The network address of the peer that has sent us the query.
        :param data: The serialized, raw data in the packet.
        """
        my_txs = self.database.get_my_latest_transactions(
            limit=self.settings.max_tx_returned_in_query)
        self.logger.debug("Sending %d bandwidth transaction(s) to peer %s:%d",
                          len(my_txs), *source_address)
        for tx in my_txs:
            self.send_transaction(tx, source_address, 0)

    def get_statistics(self) -> Dict:
        """
        Return a dictionary with bandwidth statistics, including the total amount of bytes given and taken, and the
        number of unique peers you helped/that helped you.
        :return: A dictionary with statistics.
        """
        my_pk = self.my_peer.public_key.key_to_bin()
        return {
            "id": hexlify(my_pk),
            "total_given": self.database.get_total_given(my_pk),
            "total_taken": self.database.get_total_taken(my_pk),
            "num_peers_helped": self.database.get_num_peers_helped(my_pk),
            "num_peers_helped_by": self.database.get_num_peers_helped_by(my_pk)
        }

    async def unload(self) -> None:
        """
        Unload this community by shutting down the request cache and database.
        """
        self.logger.info("Unloading the bandwidth accounting community.")

        await self.request_cache.shutdown()
        self.database.shutdown()

        await super().unload()
Ejemplo n.º 6
0
def bandwidth_db(tmpdir, my_key):
    db = BandwidthDatabase(Path(":memory:"), my_key.pub().key_to_bin())
    yield db
    db.shutdown()