diff --git a/docker/lnd/Dockerfile b/docker/lnd/Dockerfile index 8cce0878..8b500b49 100644 --- a/docker/lnd/Dockerfile +++ b/docker/lnd/Dockerfile @@ -7,7 +7,7 @@ RUN apk update && \ apk add git # Copy in the local repository to build from. -RUN git clone https://github.com/lightningnetwork/lnd.git --branch v0.9.0-beta +RUN git clone https://github.com/lightningnetwork/lnd.git --branch v0.10.1-beta # Force Go to use the cgo based DNS resolver. This is required to ensure DNS # queries required to connect to linked containers succeed. diff --git a/itests/config.ini b/itests/config.ini index e1be2e0e..5d9375a5 100644 --- a/itests/config.ini +++ b/itests/config.ini @@ -4,6 +4,7 @@ network=simnet [lnd] rpc_host=lnd rpc_port=10009 +port=9735 [electrum] rpc_host= diff --git a/itests/generate_blocks.sh b/itests/generate_blocks.sh new file mode 100755 index 00000000..b58532b0 --- /dev/null +++ b/itests/generate_blocks.sh @@ -0,0 +1,10 @@ +#!/bin/bash + + +while true; do + alice_address=$(docker exec -it lnd_alice lncli --network=simnet newaddress np2wkh | jq .address -r) + MINING_ADDRESS=$alice_address docker-compose up -d btcd + echo "Mining 1 blocks to address: $alice_address ..." + docker-compose run btcctl generate 400 + sleep 10 +done diff --git a/itests/run_itest.sh b/itests/run_itest.sh index 2cf67c0d..e207061d 100755 --- a/itests/run_itest.sh +++ b/itests/run_itest.sh @@ -1,5 +1,17 @@ #!/bin/bash +trap "exit" INT TERM +trap "kill 0" EXIT + +function mine_blocks { + while true; do + printf "Mining 1 block to address: $MINING_ADDRESS ..." + docker-compose run btcctl generate 1 + sleep 10 + done +} + + cd itests docker-compose down --volumes docker-compose build @@ -14,6 +26,10 @@ docker-compose run btcctl generate 400 echo "Finished mining blocks." sleep 10 +echo "Continue mining blocks, 1 every 10 seconds." +mine_blocks & +echo "Background mining task is in background..." + echo "Running test.sh...." docker-compose run test ./test.sh diff --git a/itests/shell.py b/itests/shell.py new file mode 100644 index 00000000..e69de29b diff --git a/itests/test.py b/itests/test.py index e4800c47..e977dc8a 100644 --- a/itests/test.py +++ b/itests/test.py @@ -18,10 +18,10 @@ import logging import random import time - from bitcoin.core import lx, x from squeak.params import SelectParams from squeak.core import CSqueak +from squeak.core import CheckSqueak from squeak.core import HASH_LENGTH from squeak.core import MakeSqueakFromStr from squeak.core.signing import CSigningKey @@ -89,6 +89,14 @@ def load_lightning_client() -> LNDLightningClient: ) + +def bxor(b1, b2): # use xor for bytes + result = bytearray() + for b1, b2 in zip(b1, b2): + result.append(b1 ^ b2) + return bytes(result) + + def run(): # Set the network to simnet for itest. SelectParams("mainnet") @@ -102,7 +110,7 @@ def run(): lnd_lightning_client = load_lightning_client() balance_from_client = lnd_lightning_client.get_wallet_balance() print("Balance from direct client: %s" % balance_from_client) - assert balance_from_client.total_balance == 1505000000000 + assert balance_from_client.total_balance >= 1505000000000 # Make the stubs server_stub = squeak_server_pb2_grpc.SqueakServerStub(server_channel) @@ -164,6 +172,53 @@ def run(): )) assert get_hash(squeak) not in set(lookup_response.hashes) + # Buy the squeak data key + buy_response = server_stub.BuySqueak(squeak_server_pb2.BuySqueakRequest(hash=post_response.hash)) + print("Server buy response: " + str(buy_response)) + assert buy_response.offer.payment_request.startswith('ln') + + # Connect to the server lightning node + # lightning_host_port = buy_response.offer.host + ':' + buy_response.offer.port + lightning_host_port = buy_response.offer.host + # connect_peer_response = lnd_lightning_client.connect_peer(buy_response.offer.pubkey, 'lnd_sqkserver') + connect_peer_response = lnd_lightning_client.connect_peer(buy_response.offer.pubkey, lightning_host_port) + print("Server connect peer response: " + str(connect_peer_response)) + + # List peers + list_peers_response = lnd_lightning_client.list_peers() + print("Server list peers response: " + str(list_peers_response)) + + # Open channel to the server lightning node + # pubkey_bytes = bytes.fromhex(buy_response.offer.pubkey) + open_channel_response = lnd_lightning_client.open_channel_sync(buy_response.offer.pubkey, 1000000) + print("Server open channel response: " + str(open_channel_response)) + + # List channels + list_channels_response = lnd_lightning_client.list_channels() + print("Server list channels response: " + str(list_channels_response)) + + # Sleep for 30 seconds to confirm the channel open transaction + time.sleep(60) + + # List channels + list_channels_response = lnd_lightning_client.list_channels() + print("Server list channels response: " + str(list_channels_response)) + + # Pay the invoice + preimage = None + payment = lnd_lightning_client.pay_invoice_sync(buy_response.offer.payment_request) + print("Server pay invoice response: " + str(payment)) + preimage = payment.payment_preimage + print("preimage: " + str(preimage)) + + # Clear the data key from the squeak and verify with the payment preimage + new_data_key = bxor(buy_response.offer.nonce, preimage) + print("new data key: " + str(new_data_key)) + squeak.ClearDataKey() + squeak.SetDataKey(new_data_key) + CheckSqueak(squeak) + print("Finished checking squeak.") + if __name__ == '__main__': diff --git a/squeakserver/common/lnd_lightning_client.py b/squeakserver/common/lnd_lightning_client.py index 98d03100..9895dadf 100644 --- a/squeakserver/common/lnd_lightning_client.py +++ b/squeakserver/common/lnd_lightning_client.py @@ -52,3 +52,74 @@ class LNDLightningClient(): def get_wallet_balance(self): # Retrieve and display the wallet balance return self.stub.WalletBalance(self.ln_module.WalletBalanceRequest(), metadata=[('macaroon', self.macaroon)]) + + def add_invoice(self, preimage, amount): + """ Create a new invoice with the given hash pre-image. + + args: + preimage -- the preimage bytes used to create the invoice + amount -- the value of the invoice + """ + invoice = self.ln_module.Invoice( + r_preimage = preimage, + value=amount, + ) + return self.stub.AddInvoice(invoice, metadata=[('macaroon', self.macaroon)]) + + def pay_invoice_sync(self, payment_request): + """ Pay an invoice with a given payment_request + + args: + payment_request -- the payment_request as a string + """ + send_payment_request = self.ln_module.SendRequest( + payment_request=payment_request, + ) + return self.stub.SendPaymentSync(send_payment_request, metadata=[('macaroon', self.macaroon)]) + + def connect_peer(self, pubkey, host): + """ Connect to a lightning node peer. + + args: + pubkey -- The identity pubkey of the Lightning node + host -- The network location of the lightning node + """ + lightning_address = self.ln_module.LightningAddress( + pubkey=pubkey, + host=host, + ) + connect_peer_request = self.ln_module.ConnectPeerRequest( + addr=lightning_address, + ) + return self.stub.ConnectPeer(connect_peer_request, metadata=[('macaroon', self.macaroon)]) + + def get_info(self): + """ Get info about the lightning network node. + """ + get_info_request = self.ln_module.GetInfoRequest() + return self.stub.GetInfo(get_info_request, metadata=[('macaroon', self.macaroon)]) + + def open_channel_sync(self, pubkey_str, local_amount): + """ Open a channel with a remote lightning node. + + args: + pubkey (str) -- The identity pubkey of the Lightning node + local_amount -- The number of satoshis the wallet should commit to the channel + """ + open_channel_request = self.ln_module.OpenChannelRequest( + node_pubkey_string=pubkey_str, + local_funding_amount=local_amount, + ) + return self.stub.OpenChannelSync(open_channel_request, metadata=[('macaroon', self.macaroon)]) + + def list_channels(self): + """ List the channels + """ + list_channels_request = self.ln_module.ListChannelsRequest() + return self.stub.ListChannels(list_channels_request, metadata=[('macaroon', self.macaroon)]) + + def list_peers(self): + """ List the peers + """ + list_peers_request = self.ln_module.ListPeersRequest() + return self.stub.ListPeers(list_peers_request, metadata=[('macaroon', self.macaroon)]) diff --git a/squeakserver/common/rpc/squeak_server.proto b/squeakserver/common/rpc/squeak_server.proto index b80ca74a..5b4fb2fe 100644 --- a/squeakserver/common/rpc/squeak_server.proto +++ b/squeakserver/common/rpc/squeak_server.proto @@ -36,6 +36,10 @@ service SqueakServer { */ rpc LookupSqueaks (LookupSqueaksRequest) returns (LookupSqueaksReply) {} + /** sqk: `buysqueak` + */ + rpc BuySqueak (BuySqueakRequest) returns (BuySqueakReply) {} + } message PostSqueakRequest { @@ -57,6 +61,11 @@ message LookupSqueaksRequest { int32 max_block = 3; } +message BuySqueakRequest { + /// Hash of the squeak to buy. + bytes hash = 1; +} + message PostSqueakReply { /// The squeak. bytes hash = 1; @@ -72,6 +81,11 @@ message LookupSqueaksReply { repeated bytes hashes = 1; } +message BuySqueakReply { + /// The buy offer + SqueakBuyOffer offer = 1; +} + message Squeak { /// Hash of the squeak. bytes hash = 1; @@ -79,3 +93,29 @@ message Squeak { /// Serialized squeak. bytes serialized_squeak = 2; } + +message SqueakBuyOffer { + /// The squeak hash. + bytes squeak_hash = 1; + + /// The decryption nonce + bytes nonce = 2; + + /// The amount + int64 amount = 3; + + /// The invoice preimage hash. + bytes preimage_hash = 4; + + /// The invoice + string payment_request = 5; + + /// The seller node pubkey + string pubkey = 6; + + /// The host of the seller lightning node + string host = 7; + + /// The port of the seller lightning node + string port = 8; +} diff --git a/squeakserver/server/buy_offer.py b/squeakserver/server/buy_offer.py new file mode 100644 index 00000000..dd89ed1b --- /dev/null +++ b/squeakserver/server/buy_offer.py @@ -0,0 +1,13 @@ + + +class BuyOffer(): + + def __init__(self, squeak_hash, nonce, amount, preimage_hash, payment_request, pubkey, host, port): + self.squeak_hash = squeak_hash + self.nonce = nonce + self.amount = amount + self.preimage_hash = preimage_hash + self.payment_request = payment_request + self.pubkey = pubkey + self.host = host + self.port = port diff --git a/squeakserver/server/lightning_address.py b/squeakserver/server/lightning_address.py new file mode 100644 index 00000000..d1ae7be0 --- /dev/null +++ b/squeakserver/server/lightning_address.py @@ -0,0 +1,4 @@ +from collections import namedtuple + + +LightningAddressHostPort = namedtuple('LightningAddress', ['host', 'port']) diff --git a/squeakserver/server/main.py b/squeakserver/server/main.py index 514d4ca1..2f1d5bdc 100644 --- a/squeakserver/server/main.py +++ b/squeakserver/server/main.py @@ -1,3 +1,4 @@ +import socket import argparse import logging import signal @@ -14,6 +15,7 @@ import squeakserver.common.rpc.lnd_pb2 as ln import squeakserver.common.rpc.lnd_pb2_grpc as lnrpc from squeakserver.common.lnd_lightning_client import LNDLightningClient +from squeakserver.server.lightning_address import LightningAddressHostPort from squeakserver.server.squeak_server_servicer import SqueakServerServicer from squeakserver.server.squeak_server_handler import SqueakServerHandler from squeakserver.server.db_params import parse_db_params @@ -30,6 +32,17 @@ def load_lightning_client(config) -> LNDLightningClient: ) +def load_lightning_host_port(config) -> LNDLightningClient: + lnd_ip_address = socket.gethostbyname( + config['lnd']['rpc_host'], + ) + lnd_port = config['lnd']['port'] + return LightningAddressHostPort( + lnd_ip_address, + lnd_port, + ) + + def load_rpc_server(config, handler) -> SqueakServerServicer: return SqueakServerServicer( config['server']['rpc_host'], @@ -51,8 +64,9 @@ def start_rpc_server(handler): server.serve() -def load_handler(lightning_client, postgres_db): +def load_handler(lightning_host_port, lightning_client, postgres_db): return SqueakServerHandler( + lightning_host_port, lightning_client, postgres_db ) @@ -142,8 +156,9 @@ def run_server(config): print('starting lightning client here...', flush=True) lightning_client = load_lightning_client(config) + lightning_host_port = load_lightning_host_port(config) # db_factory = load_db_factory(config) - handler = load_handler(lightning_client, postgres_db) + handler = load_handler(lightning_host_port, lightning_client, postgres_db) # start rpc server # start_rpc_server(handler) diff --git a/squeakserver/server/squeak_server_handler.py b/squeakserver/server/squeak_server_handler.py index 71e83269..ca59a89a 100644 --- a/squeakserver/server/squeak_server_handler.py +++ b/squeakserver/server/squeak_server_handler.py @@ -5,7 +5,11 @@ from squeak.core.signing import CSigningKey from squeak.core.signing import CSqueakAddress from squeakserver.common.lnd_lightning_client import LNDLightningClient +from squeakserver.server.buy_offer import BuyOffer from squeakserver.server.postgres_db import PostgresDb +from squeakserver.server.util import generate_offer_nonce +from squeakserver.server.util import bxor +from squeakserver.server.lightning_address import LightningAddressHostPort logger = logging.getLogger(__name__) @@ -17,9 +21,11 @@ class SqueakServerHandler(object): def __init__( self, + lightning_host_port: LightningAddressHostPort, lightning_client: LNDLightningClient, postgres_db: PostgresDb, ) -> None: + self.lightning_host_port = lightning_host_port self.lightning_client = lightning_client self.postgres_db = postgres_db @@ -42,3 +48,41 @@ class SqueakServerHandler(object): hashes = self.postgres_db.lookup_squeaks(addresses, min_block, max_block) logger.info("Got hashes from db: " + str(hashes)) return hashes + + def handle_buy_squeak(self, squeak_hash): + logger.info("Handler buy squeak by hash: " + str(squeak_hash)) + + # Get the squeak from the database + squeak = self.postgres_db.get_squeak(squeak_hash) + # Get the datakey from the squeak + data_key = squeak.GetDataKey() + # Generate a new random offer nonce + nonce = generate_offer_nonce() + # Get the invoice preimage from the nonce and the squeak data key + logger.info("Handling buy with nonce: " + str(nonce)) + logger.info("Handling buy with data_key: " + str(data_key)) + preimage = bxor(nonce, data_key) + # TODO: Get the offer price + amount = 100 + + logger.info("Handling buy with preimage: " + str(preimage)) + # Create the lightning invoice + add_invoice_response = self.lightning_client.add_invoice(preimage, amount) + preimage_hash = add_invoice_response.r_hash + invoice_payment_request = add_invoice_response.payment_request + + # 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, + nonce, + amount, + preimage_hash, + invoice_payment_request, + pubkey, + self.lightning_host_port.host, + self.lightning_host_port.port, + ) diff --git a/squeakserver/server/squeak_server_servicer.py b/squeakserver/server/squeak_server_servicer.py index 427e1d76..83301936 100644 --- a/squeakserver/server/squeak_server_servicer.py +++ b/squeakserver/server/squeak_server_servicer.py @@ -85,6 +85,41 @@ class SqueakServerServicer(squeak_server_pb2_grpc.SqueakServerServicer): hashes=hashes, ) + def BuySqueak(self, request, context): + squeak_hash = request.hash + # TODO: check if hash is valid + + buy_response = self.handler.handle_buy_squeak(squeak_hash) + + if buy_response == None: + context.set_code(grpc.StatusCode.INVALID_ARGUMENT) + return squeak_server_pb2.BuySqueakReply( + offer=None, + ) + + offer_squeak_hash = buy_response.squeak_hash + amount = buy_response.amount + nonce = buy_response.nonce + + if offer_squeak_hash != squeak_hash: + context.set_code(grpc.StatusCode.INVALID_ARGUMENT) + return squeak_server_pb2.BuySqueakReply( + offer=None, + ) + + return squeak_server_pb2.BuySqueakReply( + offer=squeak_server_pb2.SqueakBuyOffer( + squeak_hash=offer_squeak_hash, + nonce=nonce, + amount=amount, + preimage_hash=buy_response.preimage_hash, + payment_request=buy_response.payment_request, + pubkey=buy_response.pubkey, + host=buy_response.host, + port=buy_response.port, + ) + ) + def serve(self): print('Calling serve...', flush=True) server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) diff --git a/squeakserver/server/util.py b/squeakserver/server/util.py index 52f473d0..d971fe21 100644 --- a/squeakserver/server/util.py +++ b/squeakserver/server/util.py @@ -1,4 +1,19 @@ +import os + + +DATA_KEY_LENGTH = 32 def get_hash(squeak): return squeak.GetHash()[::-1] + + +def generate_offer_nonce(): + return os.urandom(DATA_KEY_LENGTH) + + +def bxor(b1, b2): # use xor for bytes + result = bytearray() + for b1, b2 in zip(b1, b2): + result.append(b1 ^ b2) + return bytes(result)