From 138ef7d98471353434a4b4cb2b9de92c76dee9ee Mon Sep 17 00:00:00 2001 From: Jonathan Zernik Date: Sun, 10 Jan 2021 09:31:41 -0800 Subject: [PATCH] Add squeak core module (#587) * Add squeak_core module * Remove unused import * Delete squeakblockverifier * Use squeak_core to make squeak * Remove blockchain client from controller * Move create offer method to squeak core * Add some type hints * Move get buy offer method to squeak_core * Delete commented old get buy offer block * Add pay_offer to squeak_core * Add get received payments generator function to squak core * Add get_offer method to squeak_core * Remove lightning_client property from peer_task * Remove old comments from payment verifier class * Use bytes internally for payment_hash * Represent secret_key as bytes internally --- itests/docker-compose.yml | 16 ++ squeaknode/admin/util.py | 8 +- squeaknode/core/offer.py | 4 +- squeaknode/core/sent_payment.py | 2 +- squeaknode/core/squeak_controller.py | 131 +++--------- squeaknode/core/squeak_core.py | 251 +++++++++++++++++++++++ squeaknode/db/squeak_db.py | 32 +-- squeaknode/main.py | 12 +- squeaknode/node/sent_offers_verifier.py | 57 ++--- squeaknode/node/squeak_block_verifier.py | 23 --- squeaknode/node/squeak_store.py | 9 +- squeaknode/sync/peer_task.py | 60 +----- tests/core/test_squeak_controller.py | 23 +-- tests/core/test_squeak_core.py | 94 +++++++++ 14 files changed, 443 insertions(+), 279 deletions(-) create mode 100644 squeaknode/core/squeak_core.py delete mode 100644 squeaknode/node/squeak_block_verifier.py create mode 100644 tests/core/test_squeak_core.py diff --git a/itests/docker-compose.yml b/itests/docker-compose.yml index dc49c272..9f10b6a9 100644 --- a/itests/docker-compose.yml +++ b/itests/docker-compose.yml @@ -81,6 +81,17 @@ services: - ../createdb.sql:/docker-entrypoint-initdb.d/init.sql - squeaknode_pgdata:/var/lib/postgresql + squeaknode_other_db: + image: postgres + container_name: test_squeaknode_other_db + environment: + - POSTGRES_DB=postgres + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + volumes: + - ../createdb.sql:/docker-entrypoint-initdb.d/init.sql + - squeaknode_other_pgdata:/var/lib/postgresql + squeaknode: image: squeaknode container_name: test_squeaknode @@ -110,6 +121,8 @@ services: build: context: ../ dockerfile: Dockerfile + environment: + - SQUEAKNODE_DB_CONNECTION_STRING=postgresql://postgres:postgres@squeaknode_other_db/squeaknode volumes: - test_shared:/rpc - test_lnd_client_dir:/root/.lnd @@ -117,6 +130,7 @@ services: links: - "btcd:btcd" - "lnd_client:lnd" + - "squeaknode_other_db:squeaknode_other_db" sysctls: - net.ipv6.conf.all.disable_ipv6=0 entrypoint: ["./start-squeaknode.sh"] @@ -155,3 +169,5 @@ volumes: # postgres db data squeaknode_pgdata: driver: local + squeaknode_other_pgdata: + driver: local diff --git a/squeaknode/admin/util.py b/squeaknode/admin/util.py index fbbecffb..2573110b 100644 --- a/squeaknode/admin/util.py +++ b/squeaknode/admin/util.py @@ -93,7 +93,7 @@ def sent_payment_with_peer_to_message(sent_payment_with_peer): peer_name=peer.peer_name, squeak_hash=sent_payment.squeak_hash.hex(), payment_hash=sent_payment.payment_hash.hex(), - secret_key=sent_payment.secret_key, + secret_key=sent_payment.secret_key.hex(), price_msat=sent_payment.price_msat, node_pubkey=sent_payment.node_pubkey, time_ms=int(sent_payment.created.timestamp()) * 1000, @@ -136,8 +136,8 @@ def sent_offer_to_message(sent_offer): return squeak_admin_pb2.SentOffer( sent_offer_id=sent_offer.sent_offer_id, squeak_hash=sent_offer.squeak_hash.hex(), - payment_hash=sent_offer.payment_hash, - secret_key=sent_offer.secret_key, + payment_hash=sent_offer.payment_hash.hex(), + secret_key=sent_offer.secret_key.hex(), nonce=sent_offer.nonce.hex(), price_msat=sent_offer.price_msat, ) @@ -149,7 +149,7 @@ def received_payments_to_message(received_payment): return squeak_admin_pb2.ReceivedPayment( received_payment_id=received_payment.received_payment_id, squeak_hash=received_payment.squeak_hash.hex(), - payment_hash=received_payment.payment_hash, + payment_hash=received_payment.payment_hash.hex(), price_msat=received_payment.price_msat, payment_time_ms=int(received_payment.created.timestamp()) * 1000, client_addr=received_payment.client_addr, diff --git a/squeaknode/core/offer.py b/squeaknode/core/offer.py index 27db9714..e7a1d6c9 100644 --- a/squeaknode/core/offer.py +++ b/squeaknode/core/offer.py @@ -6,9 +6,9 @@ class Offer(NamedTuple): """Class for saving an offer from a remote peer.""" offer_id: Optional[int] squeak_hash: bytes - price_msat: bytes + price_msat: int payment_hash: bytes - nonce: str + nonce: bytes payment_point: bytes invoice_timestamp: int invoice_expiry: int diff --git a/squeaknode/core/sent_payment.py b/squeaknode/core/sent_payment.py index 9d84e35d..ea59929f 100644 --- a/squeaknode/core/sent_payment.py +++ b/squeaknode/core/sent_payment.py @@ -12,4 +12,4 @@ class SentPayment(NamedTuple): payment_hash: bytes secret_key: bytes price_msat: int - node_pubkey: bytes + node_pubkey: str diff --git a/squeaknode/core/squeak_controller.py b/squeaknode/core/squeak_controller.py index e752e54b..4f2187c8 100644 --- a/squeaknode/core/squeak_controller.py +++ b/squeaknode/core/squeak_controller.py @@ -6,21 +6,15 @@ from squeak.core import CSqueak from squeak.core.signing import CSigningKey from squeak.core.signing import CSqueakAddress -from squeaknode.core.buy_offer import BuyOffer +from proto import squeak_server_pb2 from squeaknode.core.offer import Offer -from squeaknode.core.sent_offer import SentOffer -from squeaknode.core.sent_payment import SentPayment from squeaknode.core.squeak_address_validator import SqueakAddressValidator from squeaknode.core.squeak_peer import SqueakPeer from squeaknode.core.squeak_profile import SqueakProfile -from squeaknode.core.util import add_tweak -from squeaknode.core.util import generate_tweak -from squeaknode.core.util import subtract_tweak from squeaknode.node.received_payments_subscription_client import ( OpenReceivedPaymentsSubscriptionClient, ) from squeaknode.node.sent_offers_verifier import SentOffersVerifier -from squeaknode.node.squeak_maker import SqueakMaker logger = logging.getLogger(__name__) @@ -29,20 +23,18 @@ class SqueakController: def __init__( self, squeak_db, - blockchain_client, - lightning_client, + squeak_core, squeak_store, squeak_whitelist, config, ): self.squeak_db = squeak_db - self.blockchain_client = blockchain_client - self.lightning_client = lightning_client + self.squeak_core = squeak_core self.squeak_store = squeak_store self.squeak_whitelist = squeak_whitelist self.sent_offers_verifier = SentOffersVerifier( self.squeak_db, - self.lightning_client, + self.squeak_core, ) self.config = config @@ -64,59 +56,10 @@ class SqueakController: def get_buy_offer(self, squeak_hash: bytes, client_addr: str): # Check if there is an existing offer for the hash/client_addr combination sent_offer = self.get_saved_sent_offer(squeak_hash, client_addr) - # Get the lightning network node pubkey - get_info_response = self.lightning_client.get_info() - pubkey = get_info_response.identity_pubkey - # Return the buy offer - return BuyOffer( - squeak_hash=squeak_hash, - price_msat=self.config.core.price_msat, - nonce=sent_offer.nonce, - payment_request=sent_offer.payment_request, - pubkey=pubkey, - host=self.config.lnd.external_host, - port=self.config.lnd.port, - ) - - def create_offer(self, squeak_hash: bytes, client_addr: str): - # Generate a new random nonce - nonce = generate_tweak() - # Get the squeak secret key - squeak = self.squeak_store.get_squeak(squeak_hash) - secret_key = squeak.GetDecryptionKey() - # Calculate the preimage - # preimage = bxor(nonce, secret_key) - preimage = add_tweak(secret_key, nonce) - logger.info( - "Create offer with secret key: {} nonce: {} preimage: {}".format( - secret_key, nonce, preimage - ) - ) - # Create the lightning invoice - add_invoice_response = self.lightning_client.add_invoice( - preimage, self.config.core.price_msat - ) - logger.info("add_invoice_response: {}".format(add_invoice_response)) - payment_hash = add_invoice_response.r_hash - invoice_payment_request = add_invoice_response.payment_request - # invoice_expiry = add_invoice_response.expiry - lookup_invoice_response = self.lightning_client.lookup_invoice( - payment_hash.hex() - ) - invoice_time = lookup_invoice_response.creation_date - invoice_expiry = lookup_invoice_response.expiry - # Save the incoming potential payment in the databse. - return SentOffer( - sent_offer_id=None, - squeak_hash=squeak_hash, - payment_hash=payment_hash.hex(), - secret_key=preimage.hex(), - nonce=nonce, - price_msat=self.config.core.price_msat, - payment_request=invoice_payment_request, - invoice_time=invoice_time, - invoice_expiry=invoice_expiry, - client_addr=client_addr, + return self.squeak_core.create_buy_offer( + sent_offer, + self.config.lnd.external_host, + self.config.lnd.port, ) def get_saved_sent_offer(self, squeak_hash: bytes, client_addr: str): @@ -127,7 +70,14 @@ class SqueakController: ) if sent_offer: return sent_offer - sent_offer = self.create_offer(squeak_hash, client_addr) + squeak = self.squeak_store.get_squeak(squeak_hash) + # sent_offer = self.create_offer( + # squeak, client_addr, self.config.core.price_msat) + sent_offer = self.squeak_core.create_offer( + squeak, + client_addr, + self.config.core.price_msat, + ) self.squeak_db.insert_sent_offer(sent_offer) return sent_offer @@ -189,10 +139,9 @@ class SqueakController: def make_squeak(self, profile_id: int, content_str: str, replyto_hash: bytes): squeak_profile = self.squeak_db.get_profile(profile_id) - squeak_maker = SqueakMaker(self.blockchain_client) - squeak = squeak_maker.make_squeak( + squeak_entry = self.squeak_core.make_squeak( squeak_profile, content_str, replyto_hash) - return self.save_created_squeak(squeak) + return self.save_created_squeak(squeak_entry.squeak) def get_squeak_entry_with_profile(self, squeak_hash: bytes): return self.squeak_store.get_squeak_entry_with_profile(squeak_hash) @@ -258,42 +207,10 @@ class SqueakController: offer_with_peer = self.squeak_db.get_offer_with_peer(offer_id) offer = offer_with_peer.offer logger.info("Paying offer: {}".format(offer)) - - # Pay the invoice - payment = self.lightning_client.pay_invoice_sync(offer.payment_request) - preimage = payment.payment_preimage - - if not preimage: - raise Exception( - "Payment failed with error: {}".format(payment.payment_error) - ) - - # Calculate the secret key - nonce = offer.nonce - # secret_key = bxor(nonce, preimage) - secret_key = subtract_tweak(preimage, nonce) - logger.info( - "Pay offer with secret key: {} nonce: {} preimage: {}".format( - secret_key, nonce, preimage - ) - ) - - # Save the preimage of the sent payment - sent_payment = SentPayment( - sent_payment_id=None, - created=None, - offer_id=offer_id, - peer_id=offer.peer_id, - squeak_hash=offer.squeak_hash, - payment_hash=offer.payment_hash, - secret_key=secret_key.hex(), - price_msat=offer.price_msat, - node_pubkey=offer.destination, - ) + sent_payment = self.squeak_core.pay_offer(offer) sent_payment_id = self.squeak_db.insert_sent_payment(sent_payment) - + secret_key = sent_payment.secret_key self.unlock_squeak(offer, secret_key) - return sent_payment_id def unlock_squeak(self, offer: Offer, secret_key: bytes): @@ -350,10 +267,10 @@ class SqueakController: yield payment def get_best_block_height(self): - block_info = self.blockchain_client.get_best_block_info() - return block_info.block_height + return self.squeak_core.get_best_block_height() def get_network(self): - print(self.config) - print(self.config.core) return self.config.core.network + + def get_offer(self, squeak: CSqueak, offer_msg: squeak_server_pb2.SqueakBuyOffer, peer: SqueakPeer) -> Offer: + return self.squeak_core.get_offer(squeak, offer_msg, peer) diff --git a/squeaknode/core/squeak_core.py b/squeaknode/core/squeak_core.py new file mode 100644 index 00000000..9d5a4e5e --- /dev/null +++ b/squeaknode/core/squeak_core.py @@ -0,0 +1,251 @@ +import logging +import time +from typing import Iterator +from typing import Optional + +from squeak.core import CSqueak +from squeak.core import MakeSqueakFromStr +from squeak.core.signing import CSigningKey + +from proto import squeak_server_pb2 +from squeaknode.core.buy_offer import BuyOffer +from squeaknode.core.offer import Offer +from squeaknode.core.received_payment import ReceivedPayment +from squeaknode.core.sent_offer import SentOffer +from squeaknode.core.sent_payment import SentPayment +from squeaknode.core.squeak_entry import SqueakEntry +from squeaknode.core.squeak_peer import SqueakPeer +from squeaknode.core.squeak_profile import SqueakProfile +from squeaknode.core.util import add_tweak +from squeaknode.core.util import generate_tweak +from squeaknode.core.util import get_hash +from squeaknode.core.util import subtract_tweak +from squeaknode.node.received_payments_subscription_client import ( + OpenReceivedPaymentsSubscriptionClient, +) + + +logger = logging.getLogger(__name__) + + +class SqueakCore: + def __init__( + self, + blockchain_client, + lightning_client, + ): + self.blockchain_client = blockchain_client + self.lightning_client = lightning_client + + def make_squeak(self, signing_profile: SqueakProfile, content_str: str, replyto_hash: Optional[bytes] = None) -> SqueakEntry: + if signing_profile.private_key is None: + raise Exception("Can't make squeak with a contact profile.") + signing_key_str = signing_profile.private_key.decode() + signing_key = CSigningKey(signing_key_str) + block_info = self.blockchain_client.get_best_block_info() + block_height = block_info.block_height + block_hash = block_info.block_hash + timestamp = int(time.time()) + if replyto_hash is None or len(replyto_hash) == 0: + squeak = MakeSqueakFromStr( + signing_key, + content_str, + block_height, + block_hash, + timestamp, + ) + else: + squeak = MakeSqueakFromStr( + signing_key, + content_str, + block_height, + block_hash, + timestamp, + replyto_hash, + ) + return SqueakEntry( + squeak=squeak, + block_header=block_info.block_header, + ) + + def validate_squeak(self, squeak: CSqueak) -> SqueakEntry: + block_info = self.blockchain_client.get_block_info_by_height( + squeak.nBlockHeight) + if squeak.hashBlock != block_info.block_hash: + raise Exception("Block hash incorrect.") + return SqueakEntry( + squeak=squeak, + block_header=block_info.block_header, + ) + + def get_best_block_height(self) -> int: + block_info = self.blockchain_client.get_best_block_info() + return block_info.block_height + + def create_offer(self, squeak: CSqueak, client_addr: str, price_msat: int) -> SentOffer: + # Get the squeak hash + squeak_hash = get_hash(squeak) + # Generate a new random nonce + nonce = generate_tweak() + # Get the squeak secret key + secret_key = squeak.GetDecryptionKey() + # Calculate the preimage + # preimage = bxor(nonce, secret_key) + preimage = add_tweak(secret_key, nonce) + # Create the lightning invoice + add_invoice_response = self.lightning_client.add_invoice( + preimage, price_msat + ) + payment_hash = add_invoice_response.r_hash + invoice_payment_request = add_invoice_response.payment_request + # invoice_expiry = add_invoice_response.expiry + lookup_invoice_response = self.lightning_client.lookup_invoice( + payment_hash.hex() + ) + invoice_time = lookup_invoice_response.creation_date + invoice_expiry = lookup_invoice_response.expiry + # Save the incoming potential payment in the databse. + return SentOffer( + sent_offer_id=None, + squeak_hash=squeak_hash, + payment_hash=payment_hash, + secret_key=preimage, + nonce=nonce, + price_msat=price_msat, + payment_request=invoice_payment_request, + invoice_time=invoice_time, + invoice_expiry=invoice_expiry, + client_addr=client_addr, + ) + + def create_buy_offer(self, sent_offer: SentOffer, lnd_external_host: str, lnd_port: int) -> BuyOffer: + # Get the lightning network node pubkey + get_info_response = self.lightning_client.get_info() + pubkey = get_info_response.identity_pubkey + # Return the buy offer + return BuyOffer( + squeak_hash=sent_offer.squeak_hash, + price_msat=sent_offer.price_msat, + nonce=sent_offer.nonce, + payment_request=sent_offer.payment_request, + pubkey=pubkey, + host=lnd_external_host, + port=lnd_port, + ) + + def pay_offer(self, offer: Offer) -> SentPayment: + if offer.offer_id is None: + raise Exception("Offer must have a non-null offer_id.") + # Pay the invoice + payment = self.lightning_client.pay_invoice_sync(offer.payment_request) + preimage = payment.payment_preimage + if not preimage: + raise Exception( + "Payment failed with error: {}".format(payment.payment_error) + ) + + # Calculate the secret key + nonce = offer.nonce + # secret_key = bxor(nonce, preimage) + secret_key = subtract_tweak(preimage, nonce) + # Save the preimage of the sent payment + return SentPayment( + sent_payment_id=None, + created=None, + offer_id=offer.offer_id, + peer_id=offer.peer_id, + squeak_hash=offer.squeak_hash, + payment_hash=offer.payment_hash, + secret_key=secret_key, + price_msat=offer.price_msat, + node_pubkey=offer.destination, + ) + + def get_received_payments(self, get_sent_offer_fn, latest_settle_index, retry_s) -> Iterator[ReceivedPayment]: + try: + for invoice in self.lightning_client.subscribe_invoices( + settle_index=latest_settle_index, + ): + if invoice.settled: + payment_hash = invoice.r_hash + settle_index = invoice.settle_index + # sent_offer = self.squeak_db.get_sent_offer_by_payment_hash( + # payment_hash) + sent_offer = get_sent_offer_fn(payment_hash) + received_payment = ReceivedPayment( + received_payment_id=None, + created=None, + squeak_hash=sent_offer.squeak_hash, + payment_hash=sent_offer.payment_hash, + price_msat=sent_offer.price_msat, + settle_index=settle_index, + client_addr=sent_offer.client_addr, + ) + # self.squeak_db.insert_received_payment(received_payment) + yield received_payment + + except Exception: + logger.info( + "Unable to subscribe invoices from lnd. Retrying in " + "{} seconds.".format(retry_s), + ) + time.sleep(retry_s) + + def get_offer(self, squeak: CSqueak, offer_msg: squeak_server_pb2.SqueakBuyOffer, peer: SqueakPeer) -> Offer: + if peer.peer_id is None: + raise Exception("Peer must have a non-null peer_id.") + + # Get the squeak hash + squeak_hash = get_hash(squeak) + + # Decode the payment request + pay_req = self.lightning_client.decode_pay_req( + offer_msg.payment_request) + logger.info("Decoded payment request: {}".format(pay_req)) + + squeak_payment_point = squeak.paymentPoint + payment_hash = bytes.fromhex(pay_req.payment_hash) + price_msat = pay_req.num_msat + destination = pay_req.destination + invoice_timestamp = pay_req.timestamp + invoice_expiry = pay_req.expiry + node_host = offer_msg.host or peer.host + node_port = offer_msg.port + + logger.info("price_msat: {}".format(price_msat)) + logger.info("destination: {}".format(destination)) + logger.info("invoice_timestamp: {}".format(invoice_timestamp)) + logger.info("invoice_expiry: {}".format(invoice_expiry)) + logger.info("node_host: {}".format(node_host)) + logger.info("node_port: {}".format(node_port)) + + decoded_offer = Offer( + offer_id=None, + squeak_hash=squeak_hash, + price_msat=price_msat, + payment_hash=payment_hash, + nonce=offer_msg.nonce, + payment_point=squeak_payment_point, + invoice_timestamp=invoice_timestamp, + invoice_expiry=invoice_expiry, + payment_request=offer_msg.payment_request, + destination=destination, + node_host=node_host, + node_port=node_port, + peer_id=peer.peer_id, + ) + + # TODO: Check the payment point + # payment_point = offer.payment_point + # logger.info("Payment point: {}".format(payment_point.hex())) + # expected_payment_point = squeak.paymentPoint + # logger.info("Expected payment point: {}".format(expected_payment_point.hex())) + # if payment_point != expected_payment_point: + # raise Exception( + # "Invalid offer payment point: {}, expected: {}".format( + # payment_point.hex(), + # expected_payment_point.hex(), + # ) + # ) + + return decoded_offer diff --git a/squeaknode/db/squeak_db.py b/squeaknode/db/squeak_db.py index 45703777..6cb47535 100644 --- a/squeaknode/db/squeak_db.py +++ b/squeaknode/db/squeak_db.py @@ -732,9 +732,9 @@ class SqueakDb: """ Insert a new offer. """ ins = self.offers.insert().values( squeak_hash=offer.squeak_hash.hex(), - payment_hash=offer.payment_hash, + payment_hash=offer.payment_hash.hex(), nonce=offer.nonce.hex(), - payment_point=offer.payment_point, + payment_point=offer.payment_point.hex(), invoice_timestamp=offer.invoice_timestamp, invoice_expiry=offer.invoice_expiry, price_msat=offer.price_msat, @@ -881,8 +881,8 @@ class SqueakDb: offer_id=sent_payment.offer_id, peer_id=sent_payment.peer_id, squeak_hash=sent_payment.squeak_hash.hex(), - payment_hash=sent_payment.payment_hash, - secret_key=sent_payment.secret_key, + payment_hash=sent_payment.payment_hash.hex(), + secret_key=sent_payment.secret_key.hex(), price_msat=sent_payment.price_msat, node_pubkey=sent_payment.node_pubkey, ) @@ -933,8 +933,8 @@ class SqueakDb: """ Insert a new sent offer. """ ins = self.sent_offers.insert().values( squeak_hash=sent_offer.squeak_hash.hex(), - payment_hash=sent_offer.payment_hash, - secret_key=sent_offer.secret_key, + payment_hash=sent_offer.payment_hash.hex(), + secret_key=sent_offer.secret_key.hex(), nonce=sent_offer.nonce.hex(), price_msat=sent_offer.price_msat, payment_request=sent_offer.payment_request, @@ -958,10 +958,10 @@ class SqueakDb: sent_offers = [self._parse_sent_offer(row) for row in rows] return sent_offers - def get_sent_offer_by_payment_hash(self, payment_hash): + def get_sent_offer_by_payment_hash(self, payment_hash: bytes): """ Get a sent offer by preimage hash. """ s = select([self.sent_offers]).where( - self.sent_offers.c.payment_hash == payment_hash + self.sent_offers.c.payment_hash == payment_hash.hex() ) with self.get_connection() as connection: result = connection.execute(s) @@ -1009,7 +1009,7 @@ class SqueakDb: """ Insert a new received payment. """ ins = self.received_payments.insert().values( squeak_hash=received_payment.squeak_hash.hex(), - payment_hash=received_payment.payment_hash, + payment_hash=received_payment.payment_hash.hex(), price_msat=received_payment.price_msat, settle_index=received_payment.settle_index, client_addr=received_payment.client_addr, @@ -1108,9 +1108,9 @@ class SqueakDb: return Offer( offer_id=row["offer_id"], squeak_hash=bytes.fromhex(row["squeak_hash"]), - payment_hash=row["payment_hash"], + payment_hash=bytes.fromhex(row["payment_hash"]), nonce=bytes.fromhex(row["nonce"]), - payment_point=row["payment_point"], + payment_point=bytes.fromhex(row["payment_point"]), invoice_timestamp=row["invoice_timestamp"], invoice_expiry=row["invoice_expiry"], price_msat=row["price_msat"], @@ -1140,8 +1140,8 @@ class SqueakDb: offer_id=row["offer_id"], peer_id=row["peer_id"], squeak_hash=bytes.fromhex(row["squeak_hash"]), - payment_hash=row["payment_hash"], - secret_key=row["secret_key"], + payment_hash=bytes.fromhex(row["payment_hash"]), + secret_key=bytes.fromhex(row["secret_key"]), price_msat=row["price_msat"], node_pubkey=row["node_pubkey"], ) @@ -1162,8 +1162,8 @@ class SqueakDb: return SentOffer( sent_offer_id=row["sent_offer_id"], squeak_hash=bytes.fromhex(row["squeak_hash"]), - payment_hash=row["payment_hash"], - secret_key=row["secret_key"], + payment_hash=bytes.fromhex(row["payment_hash"]), + secret_key=bytes.fromhex(row["secret_key"]), nonce=bytes.fromhex(row["nonce"]), price_msat=row["price_msat"], payment_request=row["payment_request"], @@ -1179,7 +1179,7 @@ class SqueakDb: received_payment_id=row["received_payment_id"], created=row["created"], squeak_hash=bytes.fromhex(row["squeak_hash"]), - payment_hash=row["payment_hash"], + payment_hash=bytes.fromhex(row["payment_hash"]), price_msat=row["price_msat"], settle_index=row["settle_index"], client_addr=row["client_addr"], diff --git a/squeaknode/main.py b/squeaknode/main.py index 2d7a8a2e..47c9a915 100644 --- a/squeaknode/main.py +++ b/squeaknode/main.py @@ -12,6 +12,7 @@ from squeaknode.admin.webapp.app import SqueakAdminWebServer from squeaknode.bitcoin.bitcoin_blockchain_client import BitcoinBlockchainClient from squeaknode.config.config import SqueaknodeConfig from squeaknode.core.squeak_controller import SqueakController +from squeaknode.core.squeak_core import SqueakCore from squeaknode.db.db_engine import get_engine from squeaknode.db.db_engine import get_sqlite_connection_string from squeaknode.db.squeak_db import SqueakDb @@ -19,7 +20,6 @@ from squeaknode.lightning.lnd_lightning_client import LNDLightningClient from squeaknode.node.received_payments_subscription_client import ( OpenReceivedPaymentsSubscriptionClient, ) -from squeaknode.node.squeak_block_verifier import SqueakBlockVerifier from squeaknode.node.squeak_memory_whitelist import SqueakMemoryWhitelist from squeaknode.node.squeak_node import SqueakNode from squeaknode.node.squeak_rate_limiter import SqueakRateLimiter @@ -233,7 +233,10 @@ def run_node(config): # load the blockchain client blockchain_client = load_blockchain_client(config) - squeak_block_verifier = SqueakBlockVerifier(blockchain_client) + squeak_core = SqueakCore( + blockchain_client, + lightning_client, + ) squeak_rate_limiter = SqueakRateLimiter( squeak_db, blockchain_client, @@ -245,15 +248,14 @@ def run_node(config): ) squeak_store = SqueakStore( squeak_db, - squeak_block_verifier, + squeak_core, squeak_rate_limiter, squeak_whitelist, ) squeak_controller = SqueakController( squeak_db, - blockchain_client, - lightning_client, + squeak_core, squeak_store, squeak_whitelist, config, diff --git a/squeaknode/node/sent_offers_verifier.py b/squeaknode/node/sent_offers_verifier.py index d590e5f1..68ef42a3 100644 --- a/squeaknode/node/sent_offers_verifier.py +++ b/squeaknode/node/sent_offers_verifier.py @@ -1,7 +1,7 @@ import logging -import time -from squeaknode.core.received_payment import ReceivedPayment +from squeaknode.core.sent_offer import SentOffer + logger = logging.getLogger(__name__) @@ -9,16 +9,9 @@ LND_CONNECT_RETRY_S = 10 class SentOffersVerifier: - def __init__(self, squeak_db, lightning_client): + def __init__(self, squeak_db, squeak_core): self.squeak_db = squeak_db - self.lightning_client = lightning_client - - def verify_sent_offer(self, invoice): - logger.info("Verifying invoice: {}".format(invoice)) - if invoice.settled: - payment_hash = invoice.r_hash.hex() - settle_index = invoice.settle_index - self._record_payment(payment_hash, settle_index) + self.squeak_core = squeak_core def process_subscribed_invoices(self): while True: @@ -26,39 +19,19 @@ class SentOffersVerifier: def try_processing(self): latest_settle_index = self._get_latest_settle_index() or 0 - logger.info("latest settle index: {}".format(latest_settle_index)) - try: - for invoice in self.lightning_client.subscribe_invoices( - settle_index=latest_settle_index, - ): - self.verify_sent_offer(invoice) - except Exception: - logger.info( - "Unable to subscribe invoices from lnd. Retrying in " - "{} seconds.".format(LND_CONNECT_RETRY_S), + + def get_sent_offer_for_payment_hash(payment_hash: bytes) -> SentOffer: + return self.squeak_db.get_sent_offer_by_payment_hash( + payment_hash ) - time.sleep(LND_CONNECT_RETRY_S) + + for received_payment in self.squeak_core.get_received_payments( + get_sent_offer_for_payment_hash, + latest_settle_index, + LND_CONNECT_RETRY_S, + ): + self.squeak_db.insert_received_payment(received_payment) def _get_latest_settle_index(self): logger.info("Getting latest settle index from db...") return self.squeak_db.get_latest_settle_index() - - def _record_payment(self, payment_hash, settle_index): - logger.info( - "Saving received payment for payment_hash: {} with settle_index: {}".format( - payment_hash, - settle_index, - ) - ) - sent_offer = self.squeak_db.get_sent_offer_by_payment_hash( - payment_hash) - received_payment = ReceivedPayment( - received_payment_id=None, - created=None, - squeak_hash=sent_offer.squeak_hash, - payment_hash=sent_offer.payment_hash, - price_msat=sent_offer.price_msat, - settle_index=settle_index, - client_addr=sent_offer.client_addr, - ) - self.squeak_db.insert_received_payment(received_payment) diff --git a/squeaknode/node/squeak_block_verifier.py b/squeaknode/node/squeak_block_verifier.py deleted file mode 100644 index 094c15a6..00000000 --- a/squeaknode/node/squeak_block_verifier.py +++ /dev/null @@ -1,23 +0,0 @@ -import logging - -logger = logging.getLogger(__name__) - - -class SqueakBlockVerifier: - def __init__(self, blockchain_client): - self.blockchain_client = blockchain_client - - def _get_block_info_for_height(self, block_height): - return self.blockchain_client.get_block_info_by_height(block_height) - - def get_block_header(self, squeak): - try: - block_info = self._get_block_info_for_height(squeak.nBlockHeight) - except Exception: - logger.error("Failed to get block info for squeak.", - exc_info=False) - return None - if squeak.hashBlock != block_info.block_hash: - logger.info("block hash incorrect: {}".format(block_info)) - return None - return block_info.block_header diff --git a/squeaknode/node/squeak_store.py b/squeaknode/node/squeak_store.py index eaf2f2c1..bbe84986 100644 --- a/squeaknode/node/squeak_store.py +++ b/squeaknode/node/squeak_store.py @@ -8,10 +8,10 @@ logger = logging.getLogger(__name__) class SqueakStore: def __init__( - self, squeak_db, squeak_block_verifier, squeak_rate_limiter, squeak_whitelist + self, squeak_db, squeak_core, squeak_rate_limiter, squeak_whitelist ): self.squeak_db = squeak_db - self.squeak_block_verifier = squeak_block_verifier + self.squeak_core = squeak_core self.squeak_rate_limiter = squeak_rate_limiter self.squeak_whitelist = squeak_whitelist @@ -23,10 +23,9 @@ class SqueakStore: if not self.squeak_rate_limiter.should_rate_limit_allow(squeak): raise Exception( "Excedeed allowed number of squeaks per block.") - block_header_bytes = self.squeak_block_verifier.get_block_header( - squeak) + block_info = self.squeak_core.validate_squeak(squeak) inserted_squeak_hash = self.squeak_db.insert_squeak( - squeak, block_header_bytes) + squeak, block_info.block_header) return inserted_squeak_hash def get_squeak(self, squeak_hash: bytes, clear_decryption_key: bool = False): diff --git a/squeaknode/sync/peer_task.py b/squeaknode/sync/peer_task.py index f0afbcad..46720dca 100644 --- a/squeaknode/sync/peer_task.py +++ b/squeaknode/sync/peer_task.py @@ -1,6 +1,5 @@ import logging -from squeaknode.core.offer import Offer from squeaknode.core.util import get_hash logger = logging.getLogger(__name__) @@ -27,10 +26,6 @@ class PeerSyncTask: def squeak_db(self): return self.squeak_controller.squeak_db - @property - def lightning_client(self): - return self.squeak_controller.lightning_client - @property def peer_client(self): return self.peer_connection.peer_client @@ -167,56 +162,8 @@ class PeerSyncTask: # Download the buy offer offer_msg = self._download_offer_msg(squeak_hash) - # Decode the payment request - pay_req = self._decode_payment_request(offer_msg.payment_request) - logger.info("Decoded payment request: {}".format(pay_req)) - - # TODO: Use the real payment point, not a fake value. - squeak_payment_point = squeak.paymentPoint - # payment_point = b"" - payment_hash = bytes.fromhex(pay_req.payment_hash) - price_msat = pay_req.num_msat - destination = pay_req.destination - invoice_timestamp = pay_req.timestamp - invoice_expiry = pay_req.expiry - node_host = offer_msg.host or self.peer.host - node_port = offer_msg.port - - logger.info("price_msat: {}".format(price_msat)) - logger.info("destination: {}".format(destination)) - logger.info("invoice_timestamp: {}".format(invoice_timestamp)) - logger.info("invoice_expiry: {}".format(invoice_expiry)) - logger.info("node_host: {}".format(node_host)) - logger.info("node_port: {}".format(node_port)) - - decoded_offer = Offer( - offer_id=None, - squeak_hash=squeak_hash, - price_msat=price_msat, - payment_hash=payment_hash, - nonce=offer_msg.nonce, - payment_point=squeak_payment_point, - invoice_timestamp=invoice_timestamp, - invoice_expiry=invoice_expiry, - payment_request=offer_msg.payment_request, - destination=destination, - node_host=node_host, - node_port=node_port, - peer_id=self.peer.peer_id, - ) - - # TODO: Check the payment point - # payment_point = offer.payment_point - # logger.info("Payment point: {}".format(payment_point.hex())) - # expected_payment_point = squeak.paymentPoint - # logger.info("Expected payment point: {}".format(expected_payment_point.hex())) - # if payment_point != expected_payment_point: - # raise Exception( - # "Invalid offer payment point: {}, expected: {}".format( - # payment_point.hex(), - # expected_payment_point.hex(), - # ) - # ) + decoded_offer = self.squeak_controller.get_offer( + squeak, offer_msg, self.peer) # Save the offer self._save_offer(decoded_offer) @@ -297,6 +244,3 @@ class PeerSyncTask: def _save_offer(self, offer): logger.info("Saving offer: {}".format(offer)) self.squeak_db.insert_offer(offer) - - def _decode_payment_request(self, payment_request): - return self.lightning_client.decode_pay_req(payment_request) diff --git a/tests/core/test_squeak_controller.py b/tests/core/test_squeak_controller.py index 9916b594..42afbdf2 100644 --- a/tests/core/test_squeak_controller.py +++ b/tests/core/test_squeak_controller.py @@ -1,10 +1,10 @@ import mock import pytest -from squeaknode.bitcoin.blockchain_client import BlockchainClient from squeaknode.config.config import SqueaknodeConfig from squeaknode.core.lightning_address import LightningAddressHostPort from squeaknode.core.squeak_controller import SqueakController +from squeaknode.core.squeak_core import SqueakCore from squeaknode.core.squeak_peer import SqueakPeer from squeaknode.db.squeak_db import SqueakDb from squeaknode.node.squeak_store import SqueakStore @@ -34,13 +34,8 @@ def squeak_db(): @pytest.fixture -def blockchain_client(): - return mock.Mock(spec=BlockchainClient) - - -@pytest.fixture -def lightning_client(): - return mock.Mock() +def squeak_core(): + return mock.Mock(spec=SqueakCore) @pytest.fixture @@ -71,16 +66,14 @@ def squeak_store(): @pytest.fixture def squeak_controller( squeak_db, - blockchain_client, - lightning_client, + squeak_core, squeak_store, squeak_whitelist, config, ): return SqueakController( squeak_db, - blockchain_client, - lightning_client, + squeak_core, squeak_store, squeak_whitelist, config, @@ -90,16 +83,14 @@ def squeak_controller( @pytest.fixture def regtest_squeak_controller( squeak_db, - blockchain_client, - lightning_client, + squeak_core, squeak_store, squeak_whitelist, regtest_config, ): return SqueakController( squeak_db, - blockchain_client, - lightning_client, + squeak_core, squeak_store, squeak_whitelist, regtest_config, diff --git a/tests/core/test_squeak_core.py b/tests/core/test_squeak_core.py new file mode 100644 index 00000000..4e33bfce --- /dev/null +++ b/tests/core/test_squeak_core.py @@ -0,0 +1,94 @@ +import mock +import pytest +from bitcoin.core import CoreMainParams +from squeak.core.signing import CSigningKey +from squeak.core.signing import CSqueakAddress + +from squeaknode.bitcoin.block_info import BlockInfo +from squeaknode.bitcoin.blockchain_client import BlockchainClient +from squeaknode.core.lightning_address import LightningAddressHostPort +from squeaknode.core.squeak_core import SqueakCore +from squeaknode.core.squeak_profile import SqueakProfile + + +@pytest.fixture +def lightning_client(): + return mock.Mock() + + +@pytest.fixture +def lightning_host_port(): + return LightningAddressHostPort(host="my_lightning_host", port=8765) + + +@pytest.fixture +def price_msat(): + return 777 + + +@pytest.fixture +def max_squeaks_per_address_per_hour(): + return 5000 + + +class MockBitcoinClient(BlockchainClient): + genesis_block_info = BlockInfo( + block_height=0, + block_hash=CoreMainParams.GENESIS_BLOCK.GetHash(), + block_header=CoreMainParams.GENESIS_BLOCK.serialize(), + ) + + def get_best_block_info(self) -> BlockInfo: + return self.genesis_block_info + + def get_block_info_by_height(self, block_height: int) -> BlockInfo: + if block_height == 0: + return self.genesis_block_info + else: + raise Exception("Invalid block height") + + def get_block_hash(self, block_height: int) -> bytes: + if block_height == 0: + return self.genesis_block_info.block_hash + else: + raise Exception("Invalid block height") + + def get_block_header(self, block_hash: bytes, verbose: bool) -> bytes: + if block_hash == self.genesis_block_info.block_hash: + return self.genesis_block_info.block_header + else: + raise Exception("Invalid block hash") + + +@pytest.fixture +def signing_profile(): + profile_name = "fake_name" + signing_key = CSigningKey.generate() + verifying_key = signing_key.get_verifying_key() + address = CSqueakAddress.from_verifying_key(verifying_key) + signing_key_str = str(signing_key) + signing_key_bytes = signing_key_str.encode() + return SqueakProfile( + profile_id=None, + profile_name=profile_name, + private_key=signing_key_bytes, + address=str(address), + sharing=False, + following=False, + ) + + +@pytest.fixture +def bitcoin_client(): + return MockBitcoinClient() + + +def test_make_squeak(bitcoin_client, lightning_client, signing_profile): + squeak_core = SqueakCore(bitcoin_client, lightning_client) + squeak_entry = squeak_core.make_squeak(signing_profile, "hello") + + assert squeak_entry.squeak.GetDecryptedContentStr() == "hello" + + validated_squeak_entry = squeak_core.validate_squeak(squeak_entry.squeak) + + assert validated_squeak_entry == squeak_entry